From b34e83d261f30549062ee7c8b9e25f632bbcf163 Mon Sep 17 00:00:00 2001 From: uran Date: Wed, 27 Aug 2014 13:12:42 +0300 Subject: [PATCH 001/124] Corrected validation of 'Create branch' and 'Create tag' buttons --- app/assets/javascripts/application.js.coffee | 24 ++++++++------------ app/views/projects/branches/new.html.haml | 3 ++- app/views/projects/tags/new.html.haml | 3 ++- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 4cda8b75d8..2ff64efdc5 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -76,24 +76,18 @@ window.disableButtonIfEmptyField = (field_selector, button_selector) -> # Disable button if any input field with given selector is empty window.disableButtonIfAnyEmptyField = (form, form_selector, button_selector) -> closest_submit = form.find(button_selector) - empty = false - form.find('input').filter(form_selector).each -> - empty = true if rstrip($(this).val()) is "" - - if empty - closest_submit.disable() - else - closest_submit.enable() - - form.keyup -> - empty = false + updateButtons = -> + filled = true form.find('input').filter(form_selector).each -> - empty = true if rstrip($(this).val()) is "" + filled = rstrip($(this).val()) != "" || !$(this).attr('required') - if empty - closest_submit.disable() - else + if filled closest_submit.enable() + else + closest_submit.disable() + + updateButtons() + form.keyup(updateButtons) window.sanitize = (str) -> return str.replace(/<(?:.|\n)*?>/gm, '') diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index a6623240da..2719bcc33b 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -5,7 +5,7 @@ %h3.page-title %i.fa.fa-code-fork New branch -= form_tag project_branches_path, method: :post, class: "form-horizontal" do += form_tag project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal" do .form-group = label_tag :branch_name, 'Name for new branch', class: 'control-label' .col-sm-10 @@ -19,6 +19,7 @@ = link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel' :javascript + disableButtonIfAnyEmptyField($("#new-branch-form"), ".form-control", ".btn-create"); var availableTags = #{@project.repository.ref_names.to_json}; $("#ref").autocomplete({ diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index ad7ff8d3db..289c52a2e3 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -5,7 +5,7 @@ %h3.page-title %i.fa.fa-code-fork New tag -= form_tag project_tags_path, method: :post, class: "form-horizontal" do += form_tag project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal" do .form-group = label_tag :tag_name, 'Name for new tag', class: 'control-label' .col-sm-10 @@ -25,6 +25,7 @@ = link_to 'Cancel', project_tags_path(@project), class: 'btn btn-cancel' :javascript + disableButtonIfAnyEmptyField($("#new-tag-form"), ".form-control", ".btn-create"); var availableTags = #{@project.repository.ref_names.to_json}; $("#ref").autocomplete({ From 33c9f05c6bb90a995ddc685b4a22479f17c575e5 Mon Sep 17 00:00:00 2001 From: Ciro Santilli Date: Thu, 9 Oct 2014 09:47:47 +0200 Subject: [PATCH 002/124] Append in place for strings and arrays --- app/controllers/projects/refs_controller.rb | 6 ++--- app/helpers/commits_helper.rb | 2 +- app/helpers/graph_helper.rb | 4 ++-- app/helpers/projects_helper.rb | 2 +- app/helpers/tags_helper.rb | 2 +- app/helpers/tree_helper.rb | 9 ++++--- app/models/ability.rb | 26 ++++++++++----------- app/models/concerns/mentionable.rb | 2 +- app/models/merge_request.rb | 3 ++- app/models/network/graph.rb | 2 +- app/models/project.rb | 2 +- app/models/project_team.rb | 2 +- app/models/user.rb | 4 ++-- config/application.rb | 12 +++++----- 14 files changed, 41 insertions(+), 37 deletions(-) diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 5d9336bdc4..67665f5f60 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -41,9 +41,9 @@ class Projects::RefsController < Projects::ApplicationController @path = params[:path] contents = [] - contents += tree.trees - contents += tree.blobs - contents += tree.submodules + contents.push(*tree.trees) + contents.push(*tree.blobs) + contents.push(*tree.submodules) @logs = contents[@offset, @limit].to_a.map do |content| file = @path ? File.join(@path, content.name) : content.name diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 36adeadd8a..cf27982398 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -44,7 +44,7 @@ module CommitsHelper parts = @path.split('/') parts.each_with_index do |part, i| - crumbs += content_tag(:li) do + crumbs << content_tag(:li) do # The text is just the individual part, but the link needs all the parts before it link_to part, project_commits_path(@project, tree_join(@ref, parts[0..i].join('/'))) end diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb index 7cb1b6f8d1..e1dda20de8 100644 --- a/app/helpers/graph_helper.rb +++ b/app/helpers/graph_helper.rb @@ -1,10 +1,10 @@ module GraphHelper def get_refs(repo, commit) refs = "" - refs += commit.ref_names(repo).join(" ") + refs << commit.ref_names(repo).join(' ') # append note count - refs += "[#{@graph.notes[commit.id]}]" if @graph.notes[commit.id] > 0 + refs << "[#{@graph.notes[commit.id]}]" if @graph.notes[commit.id] > 0 refs end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index e489d431e8..f7e01f9071 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -86,7 +86,7 @@ module ProjectsHelper def link_to_toggle_star(title, starred, signed_in) cls = 'star-btn' - cls += ' disabled' unless signed_in + cls << ' disabled' unless signed_in toggle_html = content_tag('span', class: 'toggle') do toggle_text = if starred diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index ef89bb32c6..fb85544df2 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -6,7 +6,7 @@ module TagsHelper def tag_list(project) html = '' project.tag_list.each do |tag| - html += link_to tag, tag_path(tag) + html << link_to(tag, tag_path(tag)) end html.html_safe diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index e32aeba5f8..8693acad99 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -10,13 +10,16 @@ module TreeHelper tree = "" # Render folders if we have any - tree += render partial: 'projects/tree/tree_item', collection: folders, locals: {type: 'folder'} if folders.present? + tree << render(partial: 'projects/tree/tree_item', collection: folders, + locals: { type: 'folder' }) if folders.present? # Render files if we have any - tree += render partial: 'projects/tree/blob_item', collection: files, locals: {type: 'file'} if files.present? + tree << render(partial: 'projects/tree/blob_item', collection: files, + locals: { type: 'file' }) if files.present? # Render submodules if we have any - tree += render partial: 'projects/tree/submodule_item', collection: submodules if submodules.present? + tree << render(partial: 'projects/tree/submodule_item', + collection: submodules) if submodules.present? tree.html_safe end diff --git a/app/models/ability.rb b/app/models/ability.rb index 97a72bf363..890417e780 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -73,28 +73,28 @@ class Ability # Rules based on role in project if team.master?(user) - rules += project_master_rules + rules.push(*project_master_rules) elsif team.developer?(user) - rules += project_dev_rules + rules.push(*project_dev_rules) elsif team.reporter?(user) - rules += project_report_rules + rules.push(*project_report_rules) elsif team.guest?(user) - rules += project_guest_rules + rules.push(*project_guest_rules) end if project.public? || project.internal? - rules += public_project_rules + rules.push(*public_project_rules) end if project.owner == user || user.admin? - rules += project_admin_rules + rules.push(*project_admin_rules) end if project.group && project.group.has_owner?(user) - rules += project_admin_rules + rules.push(*project_admin_rules) end if project.archived? @@ -193,17 +193,17 @@ class Ability # Only group masters and group owners can create new projects in group if group.has_master?(user) || group.has_owner?(user) || user.admin? - rules += [ + rules.push(*[ :create_projects, - ] + ]) end # Only group owner and administrators can manage group if group.has_owner?(user) || user.admin? - rules += [ + rules.push(*[ :manage_group, :manage_namespace - ] + ]) end rules.flatten @@ -214,10 +214,10 @@ class Ability # Only namespace owner and administrators can manage it if namespace.owner == user || user.admin? - rules += [ + rules.push(*[ :create_projects, :manage_namespace - ] + ]) end rules.flatten diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 6c1aa99668..66f83b932d 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -50,7 +50,7 @@ module Mentionable matches.each do |match| identifier = match.delete "@" if identifier == "all" - users += project.team.members.flatten + users.push(*project.team.members.flatten) else id = User.find_by(username: identifier).try(:id) users << User.find(id) unless id.blank? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 2cc427d35c..5dc3c403b3 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -248,7 +248,8 @@ class MergeRequest < ActiveRecord::Base def closes_issues if target_branch == project.default_branch issues = commits.flat_map { |c| c.closes_issues(project) } - issues += Gitlab::ClosingIssueExtractor.closed_by_message_in_project(description, project) + issues.push(*Gitlab::ClosingIssueExtractor. + closed_by_message_in_project(description, project)) issues.uniq.sort_by(&:id) else [] diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index 43979b5e80..f13a40cf4f 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -226,7 +226,7 @@ module Network reserved = [] for day in time_range - reserved += @reserved[day] + reserved.push(*@reserved[day]) end reserved.uniq! diff --git a/app/models/project.rb b/app/models/project.rb index b0c379e615..67898955d6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -170,7 +170,7 @@ class Project < ActiveRecord::Base def publicish(user) visibility_levels = [Project::PUBLIC] - visibility_levels += [Project::INTERNAL] if user + visibility_levels << Project::INTERNAL if user where(visibility_level: visibility_levels) end diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 657ee23ae2..bc9c3ce58f 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -160,7 +160,7 @@ class ProjectTeam end user_ids = project_members.pluck(:user_id) - user_ids += group_members.pluck(:user_id) if group + user_ids.push(*group_members.pluck(:user_id)) if group User.where(id: user_ids) end diff --git a/app/models/user.rb b/app/models/user.rb index 7dae318e78..33a0d03370 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -297,8 +297,8 @@ class User < ActiveRecord::Base def authorized_projects @authorized_projects ||= begin project_ids = personal_projects.pluck(:id) - project_ids += groups_projects.pluck(:id) - project_ids += projects.pluck(:id).uniq + project_ids.push(*groups_projects.pluck(:id)) + project_ids.push(*projects.pluck(:id).uniq) Project.where(id: project_ids).joins(:namespace).order('namespaces.name ASC') end end diff --git a/config/application.rb b/config/application.rb index 8a280de6fa..a7d371c78e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -12,11 +12,11 @@ module Gitlab # -- all .rb files in that directory are automatically loaded. # Custom directories with classes and modules you want to be autoloadable. - config.autoload_paths += %W(#{config.root}/lib - #{config.root}/app/models/hooks - #{config.root}/app/models/concerns - #{config.root}/app/models/project_services - #{config.root}/app/models/members) + config.autoload_paths.push(*%W(#{config.root}/lib + #{config.root}/app/models/hooks + #{config.root}/app/models/concerns + #{config.root}/app/models/project_services + #{config.root}/app/models/members)) # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named. @@ -31,7 +31,7 @@ module Gitlab config.encoding = "utf-8" # Configure sensitive parameters which will be filtered from the log file. - config.filter_parameters += [:password] + config.filter_parameters.push(*[:password]) # Enable escaping HTML in JSON. config.active_support.escape_html_entities_in_json = true From 727363163e56c25c2040057a546b171267842d6f Mon Sep 17 00:00:00 2001 From: Ivan Zotov Date: Sat, 17 Jan 2015 19:10:28 +0300 Subject: [PATCH 003/124] Optimize images 24.5% with ImageOptim. --- app/assets/images/authbuttons/github_32.png | Bin 1902 -> 1822 bytes app/assets/images/authbuttons/github_64.png | Bin 4444 -> 4196 bytes app/assets/images/authbuttons/google_32.png | Bin 1611 -> 1501 bytes app/assets/images/authbuttons/google_64.png | Bin 3437 -> 3169 bytes app/assets/images/authbuttons/twitter_32.png | Bin 1417 -> 1311 bytes app/assets/images/authbuttons/twitter_64.png | Bin 3328 -> 3054 bytes app/assets/images/bg-header.png | Bin 210 -> 90 bytes app/assets/images/bg_fallback.png | Bin 2976 -> 167 bytes app/assets/images/brand_logo.png | Bin 32119 -> 27059 bytes app/assets/images/chosen-sprite.png | Bin 396 -> 367 bytes app/assets/images/dark-scheme-preview.png | Bin 5792 -> 3996 bytes app/assets/images/diff_note_add.png | Bin 691 -> 418 bytes app/assets/images/icon-link.png | Bin 1019 -> 726 bytes app/assets/images/icon-search.png | Bin 331 -> 222 bytes app/assets/images/icon_sprite.png | Bin 2782 -> 2636 bytes app/assets/images/images.png | Bin 6644 -> 5849 bytes app/assets/images/logo-black.png | Bin 2797 -> 2608 bytes app/assets/images/logo-white.png | Bin 7501 -> 7331 bytes app/assets/images/monokai-scheme-preview.png | Bin 5401 -> 3711 bytes app/assets/images/move.png | Bin 260 -> 197 bytes app/assets/images/no_avatar.png | Bin 704 -> 621 bytes app/assets/images/no_group_avatar.png | Bin 4884 -> 942 bytes app/assets/images/slider_handles.png | Bin 4122 -> 1377 bytes .../images/solarized-dark-scheme-preview.png | Bin 4993 -> 3195 bytes .../images/solarized-light-scheme-preview.png | Bin 4746 -> 3095 bytes app/assets/images/switch_icon.png | Bin 1197 -> 231 bytes app/assets/images/trans_bg.gif | Bin 50 -> 49 bytes app/assets/images/white-scheme-preview.png | Bin 5617 -> 3751 bytes 28 files changed, 0 insertions(+), 0 deletions(-) diff --git a/app/assets/images/authbuttons/github_32.png b/app/assets/images/authbuttons/github_32.png index c56eef05eb981c1e2361312355dceecac3a7b5a4..0445b567bbce28e7983ac5c89c041e0e90bc9007 100644 GIT binary patch delta 1808 zcmV+r2k-dq4xSE>BYy_vNklKis**NR|3TuXd+vAdx$-$bpYNRW9e@1a!?x^i_YbiwCkKA~ zlrI>ndfegA3$||vqi+3b_3gEj^_ZBRg-x?5b3zED(rH*wIt9U(U|qbY?Zz7?j=ea- z+C>TMKllq@Rd{{9$DOso>2x5+LdGyHlp9Nrm zpp#=oM$@!Hr^6wgXE~kF?HanHqX<`&kTV@Q?kxH2c7Hjseq|XJFmI&r$;}8lZby}K z-WHvmrqXJQOaXX2o=pH83j{P|K(pCw9Bzl*4xNM}AcPH(!BORGYq-jCt{}H)#{;Xe zGT@U@q5P4e>nwKI?HcIMv3*QWO%AH9zmou~ZFEz2x)tQOF*!Sj*~C0DF`r6dAQtCU z>FU;1!++&)NJn;dsF5A%SUP8W3X2mE@={4!l$V1qtgXhn${@N1N05oe)(9@$=*0QY z+L(lj1aDDhC9qGStV+$&1eoFG+7R|^+lX)M+=A`vYh*}0Sx#xfNU3sz$K{kJ<5ZR% z{ZXZV`tU)#{_>0H?r7uBiip7C(2;@f?%Rcx<$q& zq+y5!Q2mS3mzu?VhVE=rt2lACQM}!7NnE_%AzE8o+4{ZY6_RY_6pP?;dl#B+^~!xF zsDFIma=SG!taNKCO989#^o@)mP#UD{cVL-VngG{48y$&HW#*^sW<5hOr7JGTgSQ}8 z>XY*+{O0H}7>05G1U6FVEAlZ+{e*#=i zU!=woBobE5Q=$!U1Z1zzEJ=T1K6gf$^?#G`@yw=12^+wJH5I517NamXTefd+e$LWW z@czYX*tumB%Kb%3H!^p5ZwTT2M;4DjOIIIGG+e})#^%gEFS}Zv$Ag+Ozw8Z_0WVHnYQ}|@4t6e2L4TLq zZS@ix8eTe#+VEOLVje`bazL{K%jp9UkYSEwgqTCOx9&k?qRh8~!s(#{s8b!k{$M#6 zMHJ$4`Ys9rxbgEt$X^|X|J#oO)3JNzfHgSC;}cigyRj68hli1?rIDDN!02!kBSV9T zkB_0GB82hD89KI;u+2@vw|Ogkn}3LWVtRQ=C^!Pvfbm(RkHfF#fQD-~@W(SBp(i>j z!_06wb;#}pprFWy$Da6}d_I2qJTTUeZI6Ew;jOEHR1(RVIShC9-*pSrA;RZ*b!oAh zg!s^5$4oFuq$oWikn7F@z<)uQg{Ir@ zPC!6drDmd}s6gpu8Rd9bC;J0~+BJHalr?r;Ydnt?-T>Baeh3vcYjEydJ>EF{8vITZ zfqXX|>mDvmuAI9kp^N|-y+ZAHRnQMcDBzQ_fQgw|ym#R$hhHUOPG}A4|MV*AcI`!| zW-YdVWgqIkycbO$pT~*gM}OJwvXW5EZ#`5ASK7Le0iKAdM4NE@y#@@%#(C6I;x8$~ zmbyo9=AAcjv*j{+BkgFveia}5`Az6fCm7y9P`X)aEZ2VU{3|~zC@Ol!ZjEGI|1PHk z#f5ob$dC(_D{7Tu=}x}>JG6Xy0V_)V7#oNpNz>G(9bZDzzyF1eb$<_|`Qvjukp7Bf zP7%;FO{dIu)ol1&c;9!PeB15GuI6!y_Y5oycCzT~pGk%U0=@0680_hS!|B4Qw|E>zxH;-RL4nQj!qVH1xlS{{kA_wIf|JfC&Hq00{s|MNUMnLSTZ{yOE3l delta 1888 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 diff --git a/app/assets/images/authbuttons/google_64.png b/app/assets/images/authbuttons/google_64.png index 4d608f710089bb406f9b3567270354434e7a58cd..94a0e089c6efe07dcb50af341d51c2d13b689639 100644 GIT binary patch delta 3165 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 Dtzq$= delta 3435 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 FV1l2fr}6** diff --git a/app/assets/images/authbuttons/twitter_64.png b/app/assets/images/authbuttons/twitter_64.png index 2893274766f8bcfe2c3ee7604d912122ca9b1be3..5c9f14cb0771005efaa2ed050560298aa03131b3 100644 GIT binary patch delta 3049 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 delta 3325 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 Hu0mjfIxQtz diff --git a/app/assets/images/bg-header.png b/app/assets/images/bg-header.png index 9ecdaf4e2d50de747f5249860f10d6debc33cf4e..639271c6fafc58a2007f22478ba2d52e57c2a2ee 100644 GIT binary patch delta 72 zcmcb_7&Sq{iGzuOfx%60!(kw$=;`7ZQo)$~k-smw^2-lKZgF${)9>%^kEr|qi*s3I bgTe~DWM4fhjJS) delta 193 zcma#L#5h5so`so#fx$Y-YB`W%E_U(^;o#u7{m}mbkjo$76XNP09B1w5ZD!+YXlAEl zXmM-qL<^u4XMsm#F#`j)5C}6~x?A@LC@53n8c`CQpH@?HG;qKe-bC?;RGZWBNMX23PM}(3rkh^g=?BA7^mX=;wk83`_j6LTK zP2}QjK6!AiDYM^#8Fyr)dsKOG`_?gVBRz;}1%{CIA2c07*qoM6N<$ Ef{v*}AOHXW literal 2976 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μtWws*I&mrRkIh6Jxc;FFc44v>F zZ>Bg7k=lAIz*{nNapTo`PR6&YOlfJIE8)*^b;$`<_kncphh0602@U4?W}SABMHAWQDJthOV#C*-8h?2Wt1m&uF=#q1~kHVDH5CEZXrKvr@ zKL+qXJ<0sIyEj-To8sIPM#%+--ugB96s?k7(`#Cf6*p?ak*eJe&ka*&68d*eU2y^{ zM+cm3Qe5O9kUkrKsc*Cbo{ehUgjHZE)?_t1YG-^|dAle%At!P&07mg>V38bj*9I2E zU;VWo9@jSKCIIWbYc?yW?p4iH?I3kfk3Wzn-3Nhu0>|Ad5sjbNL>>v%n@*v>4I
+y?CxqDj`Rq1V9&GB8+rQm+$$A%0Ag zp@TnXh^J{WYm8xj81OGEDE@5AJ8u?V$B}PLM=GpB&}?LDJN72KP8c8!UC+d(Ek>~n z&e-kcl`Uxcb>u$FCu1vg0)CH70!Qs?;S7a5phRRygt z4{iF74KSi7orbd_+S0ljn{NAtcXk)UY8Qhc<8;)3V9wCA*hY^F-YQ9y<53848gjOp zA&7B6TMuS~pV(26dx|%cpsGE%(0Q%Z57qoceGwTs~N%n+`H?A zTAzgRQ2$^nf1N>vNHJ0OKah~nAnSr~Ez31u%#Z2eY4BcAG zUX?uAwPqEMwNLt4E-GVR{2_6*y#J#B!zvBBj%xF7o9S;-pn9(d97~F4Pj?0b5D;$m zSccBPhYu3Px(N&T6YhY2s%yzB@I0PJ=It~WtTFEE?%iYbRU|j$xpt8!{*@HE6IVNZHrdisaWRd-K#7!8{VSv&d4K95>+K(a z1Fj^)a;iS?qO3UMES4K>S}H%qmeZ3^d8g%}j$s}=H)7Cbi)NftxW;^bJt|R{tF6{{ zSy@Yi>GkS;MgjSj33D7-pS}IZP^#>k5%)nJz>f|7OR8ovg8fwSAx7(?L=b7UBAx5PS zb^>*=$%oD(K-UJ9p;&;ipi3f$KNEv&q9-o{BLS%DugFS0mORgOGUo9Kp2>iY}#2F`j z4g7^qEU?UvR)!*J>@)_OkOlw3d;Af9P0S^#?}niZL^Y_Jl5$Q4deSdj7t-hS^k^Jf zkAn^(3ZJwnn{%aXCPx3$++VoYhaZ21wwF@{Ov=sUem4$^gWxzrdmosv5#lZhy|#TaS>H82P*mRjiI@U0u%M&nhUwSuxcaKyC|p1XEEN%&=$n-WKT!WoIf|b; z1hl>z%^?4h{Im7I4`+Wi-WA+zF3@+kXzK&KE2&P`&7URW<5MdFoz)G{Q0SW#+~|k5 z<(Bc@;_V+plFJO&?{fveS^G2>SP*?}T}F62_2p;=K;Ro@7m`!SmiYGQ6Q_dqT`7u~ z4^;4j3}UXw#K7lb0l54hVbj#?phT>Vy-yZKeFNhAic+d_StfM8v5%qL)j6B2?|TOS z*|gzPC!PI-0(an)SIHS=mQ;0c_bKh?zgybeU^y@;fbjB*>*^eUR51#~j;v^=vE_Lg zXw?d)Zt|P4(70J;i7Gcqc46;zB6;xWCeWm<7Koqk;Le`~c=ElUjc1fbY`OKl?ez_c znDdE%+zquJ%=Uqo3do|c+(us)zs=O>zV zNf(zEFo60;vv+HW zJ6bc~Ns@`U{41*S9hh3FcsymPA6UFpVtOk$vpgn8SBfM6D;Kc?UQ0C1aedtTA zopQd9gu$HOiM!ff3Uo4i(|YNPzIv-r{~tQOx+ISphT_d$Vt zwp@#^7*e9pY3GdDPBTiAQ_0$<_Ri|l(cA&vbB-BX*C?M?+U>yR8~DoFuS@UnxgRY~ zb*Kobulpy+#9Z{=9G0$@*w7pNJFW5=o&?@TcV}SMw)Y=*T$wNza2G_%UoVgR=yT_+ zNYdwM!P+s{AEdIs7sB_$uNgsi)_RBGoF7{QT0nXB`N>XzYazOS$26WK^OF=FtZN`l z08VfFNtgu1!Sz~4>x}`ptXyd&d@DH33t_F!A6d)Y_Hj!_XZn`Z11{%2@83zIgP z<;@PgsCgvqAF*-UoOvfjujdLVO!JkQf8yJX80EOpS?&~j!X!b6L#GBpt7abKqe|8F zFKbGm;Ab{L+`Cw!5YqRyW?if^sct7@;=APkY=i;(c+2qMF-_mqQyv(Odz>l2T*Kjb z5b`|S6Sm&bQKE2vCQ^HNe*10wS0e6ih4z=c_Doq<1;6C7eUYv?(8_%1T z|2VI@%`;&UzKwn-rIsP}dk;`Zp7>OUy&SUbdL*7&BUrzJvR|Q1GzwDrn{>=^cm!MX z`2gfuK)1g~RT^aNymcABy%%;S@?CxJLFZ)eU+_SYVMx08d)vL7cU#zH&9^5L3o_8w zi=TLt=W|D;;R@LkxO$(g%u9a^DNDUAQ}8$F{P8fKkfeF7Sjac$a&! zya~Z>aM<=cI;z$UH#8Y?x|guefzzRr|9I5YcZfY~;o>+QO&my+^|V?-cZb zctu8@T)UP7+7|f>omAnQ*qc&$$9E&pN9X+$3%z%TeLk9zyBf+mzKDP+>I&Z?_8Z<) zaznDC$^RRDs4!*Y-bM_5YL5|FyOp$}`7P=hSbpZQ<0A@0ws?eXP0oj{%sTvIZ;9_> z{3HSl`pg?s47bqaI{B3x4U)gIof4vGeNr4(Y`n140jQqM7QVrJFUgjmNcKZA` zd@2H;Rl7$G5!QDAEf9?c(X1dj?z<>^a5|HA4^@=1=e_#8EAsD-8=osLrMuh7 zyz~c}F_~$7rf_J?4_-hb=!05i_@yLoPv+)rsHCEObevaS z?whTM^XR{iuRABT_{ZWb_3s9XDQ>-`6w3P+1I3hca*^NYIg{v;%eoscWqZNGp;l-A95H+$X7!4TO_;}Zn^+h;K(~wFVU%EBgw_AIMGF)i=6O*+~zBtCKw z`s!cjnu_z5D!cyo?d_-zZ@fnhUEK3SAeaCp*U`$#^WR$lXiM0l%IBjw!?NFjAB?`U z(}PGyo&_IPBwlqxdSPEzHuRXMowuCas;k4}0};(+A(F;B#AkSFue#3;H|mXUeqS3+ z43Y0^MNbngidfcPx9}5}l=#IcA`2)8T7SL=vgA*gOTutvs^292dDMf0?gM?+K=Y%Y ze2|p%G@*|J`Q5c^$&Kx&sK5V#)$biashQ@f%EPdkmhx+RY;!s;fV6wuCaL z+h<$aX`N$niWv>9tUA_C44dWs&n3b~cXeAhWw|P(PiHwynx`qG?X^~;eadIj@@p7W zLqin5UAKnaucNZc2z@LHn)Fwf%5m-W}zsem435Px@69o`)Phw}Z>SNHQ= zr~fG$XYfv>gb<;!hZNjb6}WoxPdb#4%zgl;-ecX$UO)@!4c9zg41&4(8RVL+usC&} z%61cqR`my-q?Z?kSG~uo$&U50%w+nwr(sYe*1}mC;l+%L0`&l>36Sp#>w@PAW&3i` zTSm}peBuK0zF$BmGQ8<^V24Z!A%_#*zjRKMm^0u#X@JX_51jGcK~*)lrw+=o8F=_5 z-ObJ);dcq)Rqp$Tw6tM*e$|RkG+(xqW3@dB(N|+f4(Os1OP-0FlkXv?BEJID`3Ya> zNu4Ak@6ls6T(t=+UH#))S4fVjrz`Ar4T^XpXy;zJ#9%)2w~pU|Y!}(~hb<$7YP^eP zz-wwf2Le$h^I%sBB1Ke6KG=bNSoo^mxbkyX{{Yyvuw9=5(*)#ZEZ`#u(K9YDC_|+e z+hAUK>wqPN`FKa)d_@IamD+l|e@Cluya4nPH?t*I#uk!xPtUIDS7~Em_*l0Rouu{P zwtMoDXogGi&uB-U2yma6pJ&Anplcq$%%YN7vH)O|H|JPQrH zPn>e@tnb%2ojY?R#_(Z?ZfN4efWhmIEq66qzvT z`L7FWYhOBay`I6WZv=LplP__>3ik!0nq)a^7Ij)!ETRV`z`ekF;>(qWY$D#;`na(R z_>6;b3#dQ(iX};G`!-&A-9Z4r_Rl~BTU2Xgb*H$gEz$grp~P}2fr^j$a%jE1J-L;s zR>V)nk9DI%#YAsZk`l@($z`&hPCSppcO$dh>FJ7!dmW0XbEoaIMSFN$_YQT86tt{= zzfr>O@}2x%j5niACM`M%QYSPuOXKWo^lH4qaRXI9s+aC1RA)fN@moD}E2D*c)n8ZK zY6Vd+SJ(4Ko(!ut@*|a}YQPj>+$^j>#cFY5C|h+rC@j^wg}sH$o?a)+#M` zAbP2!vg<=~JIbX$8Ew>fK#gz}pZ}9P2SM?e5!aX}NzX8oV`1!x~Ke!tYj8 z`i5pV=tt&_n?67W`hr>yd0>%CWeQYOQl{4+r^T?Wf4K*QY1vs@Osgc~Ai4PHODpML z4Hk{LoeK0qg2EJ~l#KUTriBVyMIS03&l$Qm`^N;JRM(=9wAEWTBuYoS^=gDI(gJpq zi?#Tx)cRk+>k4-BW@F~S#fWEYh&qj+L&ZUW9Ri4KS-6L0t@NuM43!*yb%0NslW8+l zjy#g2Cx-|$S>Y;%*UTx1CA>scc;UqJzsgY=3C^>fOha@n4_Dz)TlF~`6PoQpuHt@?QxpWw!Jh1GBW49=hKS=-ph@72Jr zz1N?vn3?hg{R<u8> z1FVnzX;7=D70ffRiQXbVrMGc6Dz;F(iC|^_X?3E{h{L(BrVju=)_mA(FcemN6c}&| zpi%W)95ITZ&yY)aIIzU zuqdZG7dy!%g^yWs;L@i6=7?W@=jV?o#17o4|Lfn!tzVoRrc}`Lg*OLKk~TE+WjF~0 zG>8c;*x#)$dFIaFyD%n*A`ZCF%%%WbOCSJJ2kY@c%f+pjnQMLL`5o2@ z)~l^VPTkj=ZiTmpz1rby#Umq*K8P^7`TEgu<J6 z)^&pt*kS4BvU7$u&Vk}U?vbL5(Wj=;#XII51oRc4ZR+`qDfB>9v-eosPVIZLPL25V zDjvAM5imA*4cFpF_xqH+nqKmyyjTdDwIz2=BLfG$Y?E$Ir+ZV;Q5Z1@dvtBZf3ou1 zBm6a~U;|-(eXc&?k_T0?rcA&L5By>7;wqxR%{TYYat$O?)lZRG{EC?@@x{_6cltUk zchnbbE$|EfS=&A5v)ApUZRI3Kmhdw#Cx3opa;cF3U|nWs!M@4QkB5pZRZO}e%}Q?7)60k{z&7NMchmceWUx!#v#j1Iji7qp4p!S;VTAy`KXrF4Jue0a7&_) z-B}pcN=)PKrO3+5AW?o_nG2GUq8!u{|3O|cNheB!vA|_&v)Gw2t4J+rVtV3Ln%(Sm%R+qIXA1BSk`Xu9O8-IxN9b4sSc8Y|UD}Nt5uLr!K}zbFS9t$} zH_?+PTJJX{=-FQS>^|1^>ns23`ZXG>rkMASl2aA!9t)^+>or9P%EjDllMuBqv;;mt@NKLf{ ziC>M_nzyEC;6_zSm_G%85_11T=6cQG%^3WK$~@6AcOKRGy)xxl{YP!sOMtr>U`v36 zXrnM?4ATo-%IY|+p#a^bBYfXqNiNi;p~kMuRz1Rq0w)?M)z`)s?ruN$?*637Hrn^X2QRS@RDY3OsA5+Ng9r|N!5$^$^O3Ixq8-%a>M zUiA!hNXx4EzsC|~rm5SWd-fOtmv;4ZeWm4xIMR}gMl8kopv;Qg)qZ7W`h-ZkfVZ{OEa!^h+%jzheI!&oTgn}n1 z9NU&^*X`+#4?$TaENzRUH$I~`ZKGLx9soZL>+&mzEcxc;L#?wXi9Zl}IC7)Z0hR2S zpRCc9wACJac~n7gepeK0vi;RNNxvuClwZSWU_|gKSIq2T684P0l$q=DH_GtG7|K~q zJeywR54RCs1mEr~%<4*9mtus&8>WE@#jZ;=0wRNa`iQEn|4=j!P;#^<<|BI~_guPZ|tdYJ0lIa1XI1x3Zna=?Hid z0GYVAvi6n-6ecN^c~^gHlNN?EJNJ5n50qBvQ;V#5B$+wCVFGK0W(vI#TKb#$kel>BKK9@otQbm`om1De7#?J-ZImib%uKl4voJ+)-+ul8Go*QvFn(Rf zmmjlWxuhzI^pc5@s`snQ$DVT1-56cHSI;Nc!5BYUk(TYo zG^_-$lpKF0a3BLQOnFmzGV99#MhycZs2A`=9Qd?{kT&Q`tuh8Dee+Xfo#mw$4B`bYgSL`x!UADN$flbI87{E${Ge-( zQy~w3`L&+2&c05U{kehAcgE4AhRnGQ%ClY_pd|$Ogk69KNAChiBu_ANG4g$1xoGzj qOOF42(}R4QQchC<0CtnjH~;_vC`m*?RCodH(jkk(Fc`-1=hH?;^i#a1 z5K$EqQ&Fhu#6(5ZKEXsyp(-L!n2Cr8ESKFCu3{VIJx(0lPt^VMKok5Wv30=OMR0Uf zyH!yO4zH+Iq&zrssY{!|vX;1Z%(fw=NWHJq%O9q^Z>Yl?p?`_5uFB`U_7R&iJ)E%D z(}1UP&R?HRY)Y#lb|xa4amY+W6Lz8^q9)>yfqLwn=;4efLOlash22sUaF7ymag`u^ zOE_w&gp{IMz>%F4Lb?nVSCx2Sw^DNJT2!tapuiExlvn}E=nz)gtzHlgtP!icrSCo?kQElRBA(cDm6^~8w1?i|w9m;{E!=)&mn4u){G mmnm6Ry}AFiw&2&70mTOyFTX6N<=8U-0000003kN0ssI2j?}E!000kTNkl$|-$`#+uYy%;!d zg!`-sSfnrqo^I~u!~NVd_h6~@!OOD+Ea}=SP5ftn;`8p^gOn-w2ROBg=d+`Uvj>c| zcV$+|Po?1JqHkxWL-EJ%vj$*QU~T(tZu#-|i9+ra0gFfOKX~~n_V!KW`FA?`R{-;S z0t?~7o7me|vrj$Bie2oSoqi3B;pF{3DXZ~ofQzF+3HAIU&)=ONi`Lr`x;53g?A_0$ z;HQnz%#BOf@h0}>+#+CXB{Ca9>iwYgDWw!Pa};jp$5pku;y1<_-{IqSVQYJCV|0FO zXulZzyi4wkiLvHm6*&_oewn38U04Uk&qY`}#FJPL9Lzzkl8)uo(Pb^hexI#fZ>@h!#awVP(AV@^S~>G z=NRw3T%e5L@<4QYM$Kh6)&LBH(`5GU{@5j|ITGu4_y0~1%^bpHIw(3P+hG%DsrDi$ zghFlqUePNk?8e%FvFmfd+Xz+vK@qk!q#nwWJ0=W+MtduBEepM^#^V5dOSY12DcAu~! zUq;KkUh}HI6fj3lp_);j2(VCXj>KVz-i+_Qfq;RXud&`*N<|8qbGHD-+c*Vj5Ps+Vs|!f2HRny+b=P$tSYk%uHN0VQ)QYCVFpl9V2?go z&^|eL^Pm`@LNw?LDu9pGjMA4j4Mt z9>tQ~gBfE@z@Y1$^B%-+G5{FI0%gvZ%hc>|SYT9O7g6a8uwLB(2)4C7xc&fh)l?rG zwP;`;>|F>yAG^$B1EhNeU^w4!OUFNp+Al2N*8cB*Dm--nuDIc0 z?tS)->RbVX@u|q$H;b>MYrLi7qc~G-zAnf&Q5AVOd2Qq(d=EdIZ zi!H{3AwQ!vOeTJwN#%TYRSUH9wNucNCxap15n!p!9?4Iof1ZlPjr7E`$l{8v=-YW! z$Bh@FTd)1FnH^X0&krzHb3aT6TcO&Le6{!-0OqQgq3oWSR5&ugFHb!UjK7Xvlb)<_ zp(>QG0-X5yA5*uaI0x$sjVIzgBFd<>#PYsjP`lKgno6wzz?jOom?uZI7?|4$jmH*L zID8d!3*(P&w_#G%{Wa3AYQ#R6ukf$t5+2!khG?-j9)3E&rC|?-BLHyqWR#qmoB+=H zX8~*Nu{BFKuM8WwAmypI{E28M22a3Eo&*Hke%l{g07h85=26boN7oL}`lsRma|0$> z#9)}B8|V?7Ym&v02U2hbsnQ1nz*ID@o|Y&?b<)!S457l~kwtLaqWE?^7M5XU7k9-d zz?5-nB29(&BW;m`bdBIoVw?igJzr2Xv4^ZE3_;ji?;a&LG#VicSS zU~mx2dj>)6vQefF)<^QB9rnSBTb{^5j|f~Pu%^bRZ2V@PbaZ7rXu=u=FjqB^tC|nq zeRe~1zFG849Awci7u;lc_PIw~J)Im*-Us-92jG_WZ^uE5W%Q4rzJ(PqNS#7{Ig zyB1fDvgna=7M&UygtfQdB4g1$5q){DF|uaYO3^?E5(12b03#v5XDb942?0hzfRPYj z{KnPcqSL+av?ja7__4ooh$Xv)#D8y@nAtFVPl#P)z-{J-mHFWHA1AK0)hS?}*yivV4Zxy+#ZvEpd7T-z@4)BVcf0aK{T?A!8*ud$;rjPxf;z@~1L^Jc z@m_u%0_NiC%#d2iJhL)sQc@FNVxm7o`Lx@W8R*Yc0CREW6RT_A>n+(W^BtA<0PlQa zag@AXDgZ1g1YGb=i0AEIs^MsrOmZqJ$P~lHA*HP1-|BB{#Q}m#y*)O|J(Vu!^zOSI z$!-z1xw<0TaERbFRg6nVwaf6+0qnGRc+?FG%-;;Za}GLq#~gv2QE_>@@{w0WJnvj?wNFYOgwLMr4<6ozqP%VlG{q+09(lB%JhnHh(GTfhD{^7 zXLV#?V8`3~GIr}dVcGAW*sP2PAG5&YkPrh0cJDhQ0FO(q zL>h<5kyZe%pdxF(mh)eRRE-OBd)o_)HYXpbTfPn${8K<$Tp1O9>@jNq<{F|~zwUo{ zpPdG-Tz`N;PiYPiU1_V%jA7cV*0h2kO$M_-owE;`IW!n~+6e%2g?)KwtTcxsNnn(* zawa1-z(C4gOP=yts^L1!MT?q(3IRKwNHDt0fYn8n*@S;tCQenfL>Zegz0pxLs?GRm zgnbskn05dJ6}K#=bhG^Vj8gz6FW_m?jss*o_18w4arYkzpmAuWM zF9Vj*eK5C8SG4IWeuLdGxiSizG1dkQ8Z>D$J8+44w&;G8MHhBeZi>%b`%H1c=cnRFpCylUw)hRbkr^2+*L7-E7P{Y8&_r!VlUy^k7U+easM@Pi*dzRd>%|D>;V)<*t$HgYoJ~Gkvh*sLZ5w z5LP4(Su7z54}y_XsaXI&MGxlbdxzhF&i?pge#L|NGRNce=I)1I;b)Tn8eW&5Nn(W= z#OL4IW#!B^bG0uCTeCWk9S+g(ppnU6-qrq1imxQ67OXWOORL>Ko00!DyayY4J!pZ)vo+Ia(JSqe<^8#B*E z=@k!V73V7}iIHjp20v8!uOI)`rwgy2H(-{fzyR=fk++#~uaXOmk)>e)`$}TO^+bSi z^P}{FwXK-vF+y7#u*C6n`Js+-k-q{k`%+*4 zxcnpW;CL1sv#umYTx$bv^Q?XViwYTSMXzzs3o!dqU^c+)D~S=>lV{O?i3hVU1!e)v zvXZzF03*PxOMzJdv#%sZLL*t{AOsi*0Y+*dz(@!%5(12b03#v52yln*-z8?KayvTM zdXWGlz^#4LKdn#q@{tE$RKVajqJ44{qAx07!O)NI%kdemP3p%lDqxgk;o3mDPFEKq z)zI%)AH4Rrjtc}B0hW87ZDyzG^}*VS^fpMiE)-zo!AI+Z-)*k`;OO82BLa*DI*<@x zq(%mega9KUz(@!%5(12L@%Im|A!gsC|A&BEJ4U0~E%c7%XVmS|)o0 zaH#U9Xzia9uJ8SWfut_iVtxj0U1rfqD}dKVM#JmMjpAQ~C`GkK1>>n)c3FUl);Ix+RZ^=kVh5^8A`v>zpcI z3k7y4^+&#yVvP$};z@6>ejol<{(b4g`@*xDKk459`v>zn@8>4&-|B1^GDf!jgTYq8 z31+L{{z%@Vm9&kn_{rWD=`AvqclGgFw9$zvQ%fVyCXs9Jchg{=zw&Fdh8o`70Q(2y zT8%&MPi##kR_t(hN%sANyF=rPp^A5c-Ac(}DNVF(IO_8+`22dQ9rAAM|Ix9z>XU!@ zd(lJTqgFc8*#n@j+ftbm~hN2@)U*P@v6Kbq1{MK$gEkx)oKwdT7%t|tn) z@K9UXgV(3EH9fel_YVd=>!Xyh8&VNjT#KoGDJ$UWK3EI-;64$|sutJ$U>|p0XK=+& z1$G6m!9SBE*T$C%?|vzr`2S?p^Zvm=eC?Dkwl(u&Zzi$kvVs814jAU!_6VJC$1PeZ z{eD0000gqn-_k=1bNMoUspaTGa1(K0ae)T!8W`YX;dPdEZFT6SgM{$rUDk|#o zrsBry71v2p(@DkF%*oZz!4y#W;q2sO>R=o&fd&ACf*=VoRkx)RT~8f+jU}IS6LEHG zx;R8?WNH#o8YH|DwNI)N81;}j51YdJg^F45Qh41g*vb{7KChz9t?Wjnp4p{sW~o

" + $.each data, (key, data) -> + $.each data, (index, data) -> + $("#calendar_onclick_placeholder").append -> + "Pushed " + ((if data is null then "no" else data)) + " commit" + + ((if (data isnt 1) then "s" else "")) + + " to " + + index + "
" + return + return + return diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 3cf08782c3..8f63a7fee6 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -8,6 +8,7 @@ *= require select2 *= require_self *= require dropzone/basic + *= require cal-heatmap */ @import "main/*"; diff --git a/app/assets/stylesheets/generic/calendar.scss b/app/assets/stylesheets/generic/calendar.scss new file mode 100644 index 0000000000..9483b26164 --- /dev/null +++ b/app/assets/stylesheets/generic/calendar.scss @@ -0,0 +1,95 @@ +.calendar_onclick_placeholder { + padding: 0 0 2px 0; +} + +.calendar_commit_activity { + padding: 5px 0 0; +} + +.calendar_onclick_second { + font-size: 14px; + display: block; +} + +.calendar_onclick_hr { + padding: 0; + margin: 10px 0; +} + +.calendar_commit_date { + color: #999; +} + +.calendar_activity_summary { + font-size: 14px; +} + +/** +* This overwrites the default values of the cal-heatmap gem +*/ +.calendar { + .qi { + background-color: #999; + fill: #fff; + } + + .q1 { + background-color: #dae289; + fill: #ededed; + } + + .q2 { + background-color: #cedb9c; + fill: #ACD5F2; + } + + .q3 { + background-color: #b5cf6b; + fill: #7FA8D1; + } + + .q4 { + background-color: #637939; + fill: #49729B; + } + + .q5 { + background-color: #3b6427; + fill: #254E77; + } + + .domain-background { + fill: none; + shape-rendering: crispedges; + } + + .ch-tooltip { + position: absolute; + display: none; + margin-top: 22px; + margin-left: 1px; + font-size: 13px; + padding: 3px; + font-weight: 550; + background-color: #222; + span { + position: absolute; + width: 200px; + text-align: center; + visibility: hidden; + border-radius: 10px; + &:after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + margin-left: -8px; + width: 0; + height: 0; + border-top: 8px solid #000000; + border-right: 8px solid transparent; + border-left: 8px solid transparent; + } + } + } +} diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 67af1801bd..a5e80f7e00 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,5 +1,5 @@ class UsersController < ApplicationController - skip_before_filter :authenticate_user!, only: [:show] + skip_before_filter :authenticate_user!, only: [:show, :activities] layout :determine_layout def show @@ -10,7 +10,8 @@ class UsersController < ApplicationController end # Projects user can view - authorized_projects_ids = ProjectsFinder.new.execute(current_user).pluck(:id) + visible_projects = ProjectsFinder.new.execute(current_user) + authorized_projects_ids = visible_projects.pluck(:id) @projects = @user.personal_projects. where(id: authorized_projects_ids) @@ -24,12 +25,32 @@ class UsersController < ApplicationController @title = @user.name + user_repositories = visible_projects.map(&:repository) + @timestamps = Gitlab::CommitsCalendar.create_timestamp(user_repositories, + @user, false) + @starting_year = Gitlab::CommitsCalendar.starting_year(@timestamps) + @starting_month = Gitlab::CommitsCalendar.starting_month(@timestamps) + @last_commit_date = Gitlab::CommitsCalendar.last_commit_date(@timestamps) + respond_to do |format| format.html format.atom { render layout: false } end end + def activities + user = User.find_by_username!(params[:username]) + # Projects user can view + visible_projects = ProjectsFinder.new.execute(current_user) + + user_repositories = visible_projects.map(&:repository) + user_activities = Gitlab::CommitsCalendar.create_timestamp(user_repositories, + user, true) + user_activities = Gitlab::CommitsCalendar.commit_activity_match( + user_activities, params[:date]) + render json: user_activities.to_json + end + def determine_layout if current_user 'navless' diff --git a/app/models/repository.rb b/app/models/repository.rb index e93c76790c..e44ecca865 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -139,21 +139,46 @@ class Repository def graph_log Rails.cache.fetch(cache_key(:graph_log)) do - commits = raw_repository.log(limit: 6000, skip_merges: true, - ref: root_ref) - commits.map do |rugged_commit| - commit = Gitlab::Git::Commit.new(rugged_commit) + # handle empty repos that don't have a root_ref set yet + unless raw_repository.root_ref.present? + raw_repository.root_ref = 'refs/heads/master' + end + + commits = raw_repository.log(limit: 6000, skip_merges: true, + ref: raw_repository.root_ref) + + commits.map do |rugged_commit| + + commit = Gitlab::Git::Commit.new(rugged_commit) { author_name: commit.author_name.force_encoding('UTF-8'), author_email: commit.author_email.force_encoding('UTF-8'), additions: commit.stats.additions, - deletions: commit.stats.deletions + deletions: commit.stats.deletions, + date: commit.committed_date } end end end + def graph_logs_by_user_email(user) + graph_log.select { |u_email| u_email[:author_email] == user.email } + end + + def timestamps_by_user_from_graph_log(user) + graph_logs_by_user_email(user).map { |graph_log| graph_log[:date].to_time.to_i } + end + + def commits_log_of_user_by_date(user) + timestamps_by_user_from_graph_log(user). + group_by { |commit_date| commit_date }. + inject({}) do |hash, (timestamp_date, commits)| + hash[timestamp_date] = commits.count + hash + end + end + def cache_key(type) "#{type}:#{path_with_namespace}" end diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 6138331537..c7976ba564 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -11,5 +11,4 @@ - elsif event.note? = render "events/event/note", event: event - else - = render "events/event/common", event: event - + = render "events/event/common", event: event \ No newline at end of file diff --git a/app/views/users/_calendar.html.haml b/app/views/users/_calendar.html.haml new file mode 100644 index 0000000000..70d5cca854 --- /dev/null +++ b/app/views/users/_calendar.html.haml @@ -0,0 +1,9 @@ +#cal-heatmap.calendar + :javascript + new calendar( + #{@timestamps.to_json}, + #{@starting_year}, + #{@starting_month}, + '#{user_activities_path}' + ); += render "calendar_onclick" diff --git a/app/views/users/_calendar_onclick.html.haml b/app/views/users/_calendar_onclick.html.haml new file mode 100644 index 0000000000..1514b56bb2 --- /dev/null +++ b/app/views/users/_calendar_onclick.html.haml @@ -0,0 +1,25 @@ +#calendar_commit_activity.calendar_commit_activity + %h4.activity_title Commit Activity: + + #loading_commits + %section.text-center + %h3 + %i.icon-spinner.icon-spin + + #calendar_onclick_placeholder.calendar_onclick_placeholder + %span.calendar_onclick_second.calendar_onclick_second + - if @timestamps.empty? + %span.calendar_activity_summary + %strong> #{@user.username} +   has no activity + - else + %span.calendar_activity_summary + %strong> #{@user.username} + 's last commit was on + %span.commit_date #{@last_commit_date} + + %hr.calendar_onclick_hr + +:javascript + $("#loading_commits").hide(); + diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 54f2666ce5..0d214d3160 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -18,6 +18,8 @@ %h4 Groups: = render 'groups', groups: @groups %hr + %h4 Calendar: + = render 'calendar' %h4 User Activity: diff --git a/config/routes.rb b/config/routes.rb index f29b620e07..5d61de29b9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -157,6 +157,10 @@ Gitlab::Application.routes.draw do end end + # route for commits used by the cal-heatmap + get 'u/:username/activities' => 'users#activities', as: :user_activities, + constraints: { username: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }, + via: :get get '/u/:username' => 'users#show', as: :user, constraints: { username: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } diff --git a/lib/gitlab/commits_calendar.rb b/lib/gitlab/commits_calendar.rb new file mode 100644 index 0000000000..a862e67a59 --- /dev/null +++ b/lib/gitlab/commits_calendar.rb @@ -0,0 +1,79 @@ +module Gitlab + class CommitsCalendar + def self.create_timestamp(repositories, user, show_activity) + timestamps = {} + repositories.each do |raw_repository| + if raw_repository.exists? + commits_log = raw_repository.commits_log_of_user_by_date(user) + + populated_timestamps = + if show_activity + populate_timestamps_by_project( + commits_log, + timestamps, + raw_repository + ) + else + populate_timestamps(commits_log, timestamps) + end + timestamps.merge!(populated_timestamps) + end + end + timestamps + end + + def self.populate_timestamps(commits_log, timestamps) + commits_log.each do |timestamp_date, commits_count| + hash = { "#{timestamp_date}" => commits_count } + if timestamps.has_key?("#{timestamp_date}") + timestamps.merge!(hash) do |timestamp_date, commits_count, + new_commits_count| commits_count = commits_count.to_i + + new_commits_count + end + else + timestamps.merge!(hash) + end + end + timestamps + end + + def self.populate_timestamps_by_project(commits_log, timestamps, + project) + commits_log.each do |timestamp_date, commits_count| + if timestamps.has_key?("#{timestamp_date}") + timestamps["#{timestamp_date}"]. + merge!(project.path_with_namespace => commits_count) + else + hash = { "#{timestamp_date}" => { project.path_with_namespace => + commits_count } } + timestamps.merge!(hash) + end + end + timestamps + end + + def self.latest_commit_date(timestamps) + if timestamps.nil? || timestamps.empty? + DateTime.now.to_date + else + Time.at(timestamps.keys.first.to_i).to_date + end + end + + def self.starting_year(timestamps) + DateTime.now.to_date - 1 + end + + def self.starting_month(timestamps) + Date.today.strftime("%m").to_i + end + + def self.last_commit_date(timestamps) + latest_commit_date(timestamps).to_formatted_s(:long).to_s + end + + def self.commit_activity_match(user_activities, date) + user_activities.select { |x| Time.at(x.to_i) == Time.parse(date) } + end + end +end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb new file mode 100644 index 0000000000..bfbe5254bb --- /dev/null +++ b/spec/controllers/users_controller_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe UsersController do + let(:user) { create(:user, username: "user1", name: "User 1", email: "user1@gitlab.com") } + + before do + sign_in(user) + end + + describe "GET #show" do + render_views + before do + get :show, username: user.username + end + + it "renders the show template" do + expect(response.status).to eq(200) + expect(response).to render_template("show") + end + + it "renders calendar" do + controller.prepend_view_path 'app/views/users' + expect(response).to render_template("_calendar") + end + end +end + From ef315dda5f6b252151feb320af0abdea12690df4 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 28 Jan 2015 16:59:25 -0800 Subject: [PATCH 118/124] Add light border to bootstrap panels --- app/assets/stylesheets/gl_bootstrap.scss | 3 --- app/assets/stylesheets/main/mixins.scss | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/assets/stylesheets/gl_bootstrap.scss b/app/assets/stylesheets/gl_bootstrap.scss index 2a68d922bb..6efa56544a 100644 --- a/app/assets/stylesheets/gl_bootstrap.scss +++ b/app/assets/stylesheets/gl_bootstrap.scss @@ -1,9 +1,6 @@ /* * Twitter bootstrap with GitLab customizations/additions * - * Some unused bootstrap compontents like panels are not included. - * Other components like tabs are modified to GitLab style. - * */ $font-size-base: 13px !default; diff --git a/app/assets/stylesheets/main/mixins.scss b/app/assets/stylesheets/main/mixins.scss index 8435d1dae7..e54482d14c 100644 --- a/app/assets/stylesheets/main/mixins.scss +++ b/app/assets/stylesheets/main/mixins.scss @@ -139,7 +139,7 @@ } @mixin panel-colored { - border: none; + border: 1px solid #EEE; background: $box_bg; @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09)); From 953c1fff8f242f09f3f16998112931d48d6a5ecc Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 28 Jan 2015 17:00:40 -0800 Subject: [PATCH 119/124] Be more careful with parsing changes from gitlab-shell --- lib/gitlab/git_access.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index c7bf2efc62..ea96d04c5a 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -73,7 +73,7 @@ module Gitlab changes = changes.lines if changes.kind_of?(String) # Iterate over all changes to find if user allowed all of them to be applied - changes.each do |change| + changes.map(&:strip).reject(&:blank?).each do |change| status = change_access_check(user, project, change) unless status.allowed? # If user does not have access to make at least one change - cancel all push From c39f80bdb412bc9cc7646de0929efe8cb5b870d4 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 28 Jan 2015 23:00:41 -0800 Subject: [PATCH 120/124] Refactor commit calendar a bit. Fixed dates --- app/assets/javascripts/calendar.js.coffee | 16 ++++++++-------- app/controllers/users_controller.rb | 4 ++-- lib/gitlab/commits_calendar.rb | 8 -------- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index e3bb420a27..c5465f9207 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -4,7 +4,7 @@ class @calendar day: "numeric" year: "numeric" - constructor: (timestamps,starting_year,starting_month,activities_path) -> + constructor: (timestamps, starting_year, starting_month, activities_path) -> cal = new CalHeatMap() cal.init itemName: ["commit"] @@ -46,7 +46,7 @@ class @calendar $("#loading_commits").hide() return ), 400 - return + return return return @@ -54,17 +54,17 @@ class @calendar $("#calendar_onclick_placeholder").hide() $("#calendar_onclick_placeholder").html -> "" + - ((if nb is null then "no" else nb)) + - " commit" + - ((if (nb isnt 1) then "s" else "")) + " " + - date.toLocaleDateString("en-US", options) + + ((if nb is null then "no" else nb)) + + " commit" + + ((if (nb isnt 1) then "s" else "")) + " " + + date.toLocaleDateString("en-US", options) + "
" $.each data, (key, data) -> $.each data, (index, data) -> $("#calendar_onclick_placeholder").append -> "Pushed " + ((if data is null then "no" else data)) + " commit" + - ((if (data isnt 1) then "s" else "")) + - " to " + + ((if (data isnt 1) then "s" else "")) + + " to " + index + "
" return return diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a5e80f7e00..28de270777 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -28,8 +28,8 @@ class UsersController < ApplicationController user_repositories = visible_projects.map(&:repository) @timestamps = Gitlab::CommitsCalendar.create_timestamp(user_repositories, @user, false) - @starting_year = Gitlab::CommitsCalendar.starting_year(@timestamps) - @starting_month = Gitlab::CommitsCalendar.starting_month(@timestamps) + @starting_year = (Time.now - 1.year).strftime("%Y") + @starting_month = Date.today.strftime("%m").to_i @last_commit_date = Gitlab::CommitsCalendar.last_commit_date(@timestamps) respond_to do |format| diff --git a/lib/gitlab/commits_calendar.rb b/lib/gitlab/commits_calendar.rb index a862e67a59..93256187fd 100644 --- a/lib/gitlab/commits_calendar.rb +++ b/lib/gitlab/commits_calendar.rb @@ -60,14 +60,6 @@ module Gitlab end end - def self.starting_year(timestamps) - DateTime.now.to_date - 1 - end - - def self.starting_month(timestamps) - Date.today.strftime("%m").to_i - end - def self.last_commit_date(timestamps) latest_commit_date(timestamps).to_formatted_s(:long).to_s end From a9288e554e55e843b95ab6f8109a4c610af64c83 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 29 Jan 2015 00:53:43 -0800 Subject: [PATCH 121/124] Cleanup and make contribution calendar faster --- app/assets/javascripts/calendar.js.coffee | 42 +----------- app/controllers/users_controller.rb | 19 +----- app/models/repository.rb | 33 +++++---- app/views/users/_calendar.html.haml | 6 +- app/views/users/_calendar_onclick.html.haml | 25 ------- app/views/users/show.html.haml | 4 +- lib/gitlab/commits_calendar.rb | 74 ++++----------------- spec/controllers/users_controller_spec.rb | 4 +- 8 files changed, 42 insertions(+), 165 deletions(-) delete mode 100644 app/views/users/_calendar_onclick.html.haml diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index c5465f9207..6a0d5e4356 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -4,11 +4,13 @@ class @calendar day: "numeric" year: "numeric" - constructor: (timestamps, starting_year, starting_month, activities_path) -> + constructor: (timestamps, starting_year, starting_month) -> cal = new CalHeatMap() cal.init itemName: ["commit"] data: timestamps + domain: "year" + subDomain: "month" start: new Date(starting_year, starting_month) domainLabelFormat: "%b" id: "cal-heatmap" @@ -29,43 +31,5 @@ class @calendar ] legendCellPadding: 3 onClick: (date, count) -> - $.ajax - url: activities_path - data: - date: date - - dataType: "json" - success: (data) -> - $("#loading_commits").fadeIn() - calendar.calendarOnClick data, date, count - setTimeout (-> - $("#calendar_onclick_placeholder").fadeIn 500 - return - ), 400 - setTimeout (-> - $("#loading_commits").hide() - return - ), 400 - return return return - - @calendarOnClick: (data, date, nb)-> - $("#calendar_onclick_placeholder").hide() - $("#calendar_onclick_placeholder").html -> - "" + - ((if nb is null then "no" else nb)) + - " commit" + - ((if (nb isnt 1) then "s" else "")) + " " + - date.toLocaleDateString("en-US", options) + - "
" - $.each data, (key, data) -> - $.each data, (index, data) -> - $("#calendar_onclick_placeholder").append -> - "Pushed " + ((if data is null then "no" else data)) + " commit" + - ((if (data isnt 1) then "s" else "")) + - " to " + - index + "
" - return - return - return diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 28de270777..9e5ea6cfa4 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -25,12 +25,12 @@ class UsersController < ApplicationController @title = @user.name + # Get user repositories and collect timestamps for commits user_repositories = visible_projects.map(&:repository) - @timestamps = Gitlab::CommitsCalendar.create_timestamp(user_repositories, - @user, false) + calendar = Gitlab::CommitsCalendar.new(user_repositories, @user) + @timestamps = calendar.timestamps @starting_year = (Time.now - 1.year).strftime("%Y") @starting_month = Date.today.strftime("%m").to_i - @last_commit_date = Gitlab::CommitsCalendar.last_commit_date(@timestamps) respond_to do |format| format.html @@ -38,19 +38,6 @@ class UsersController < ApplicationController end end - def activities - user = User.find_by_username!(params[:username]) - # Projects user can view - visible_projects = ProjectsFinder.new.execute(current_user) - - user_repositories = visible_projects.map(&:repository) - user_activities = Gitlab::CommitsCalendar.create_timestamp(user_repositories, - user, true) - user_activities = Gitlab::CommitsCalendar.commit_activity_match( - user_activities, params[:date]) - render json: user_activities.to_json - end - def determine_layout if current_user 'navless' diff --git a/app/models/repository.rb b/app/models/repository.rb index e44ecca865..f6400f7aff 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -139,39 +139,36 @@ class Repository def graph_log Rails.cache.fetch(cache_key(:graph_log)) do - - # handle empty repos that don't have a root_ref set yet - unless raw_repository.root_ref.present? - raw_repository.root_ref = 'refs/heads/master' - end - - commits = raw_repository.log(limit: 6000, skip_merges: true, - ref: raw_repository.root_ref) + commits = raw_repository.log(limit: 6000, + skip_merges: true, + ref: root_ref) commits.map do |rugged_commit| - commit = Gitlab::Git::Commit.new(rugged_commit) + { author_name: commit.author_name.force_encoding('UTF-8'), author_email: commit.author_email.force_encoding('UTF-8'), additions: commit.stats.additions, deletions: commit.stats.deletions, - date: commit.committed_date } end end end - def graph_logs_by_user_email(user) - graph_log.select { |u_email| u_email[:author_email] == user.email } + def timestamps_by_user_log(user) + args = %W(git log --author=#{user.email} --since=#{(Date.today - 1.year).to_s} --pretty=format:%cd --date=short) + dates = Gitlab::Popen.popen(args, path_to_repo).first.split("\n") + + if dates.present? + dates + else + [] + end end - def timestamps_by_user_from_graph_log(user) - graph_logs_by_user_email(user).map { |graph_log| graph_log[:date].to_time.to_i } - end - - def commits_log_of_user_by_date(user) - timestamps_by_user_from_graph_log(user). + def commits_per_day_for_user(user) + timestamps_by_user_log(user). group_by { |commit_date| commit_date }. inject({}) do |hash, (timestamp_date, commits)| hash[timestamp_date] = commits.count diff --git a/app/views/users/_calendar.html.haml b/app/views/users/_calendar.html.haml index 70d5cca854..b16a7305a3 100644 --- a/app/views/users/_calendar.html.haml +++ b/app/views/users/_calendar.html.haml @@ -1,9 +1,7 @@ #cal-heatmap.calendar - :javascript + :javascript new calendar( #{@timestamps.to_json}, #{@starting_year}, - #{@starting_month}, - '#{user_activities_path}' + #{@starting_month} ); -= render "calendar_onclick" diff --git a/app/views/users/_calendar_onclick.html.haml b/app/views/users/_calendar_onclick.html.haml deleted file mode 100644 index 1514b56bb2..0000000000 --- a/app/views/users/_calendar_onclick.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -#calendar_commit_activity.calendar_commit_activity - %h4.activity_title Commit Activity: - - #loading_commits - %section.text-center - %h3 - %i.icon-spinner.icon-spin - - #calendar_onclick_placeholder.calendar_onclick_placeholder - %span.calendar_onclick_second.calendar_onclick_second - - if @timestamps.empty? - %span.calendar_activity_summary - %strong> #{@user.username} -   has no activity - - else - %span.calendar_activity_summary - %strong> #{@user.username} - 's last commit was on - %span.commit_date #{@last_commit_date} - - %hr.calendar_onclick_hr - -:javascript - $("#loading_commits").hide(); - diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 0d214d3160..c248a28047 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -18,8 +18,10 @@ %h4 Groups: = render 'groups', groups: @groups %hr + %h4 Calendar: - = render 'calendar' + %div= render 'calendar' + %hr %h4 User Activity: diff --git a/lib/gitlab/commits_calendar.rb b/lib/gitlab/commits_calendar.rb index 93256187fd..b6699c585f 100644 --- a/lib/gitlab/commits_calendar.rb +++ b/lib/gitlab/commits_calendar.rb @@ -1,71 +1,25 @@ module Gitlab class CommitsCalendar - def self.create_timestamp(repositories, user, show_activity) - timestamps = {} - repositories.each do |raw_repository| - if raw_repository.exists? - commits_log = raw_repository.commits_log_of_user_by_date(user) + attr_reader :timestamps - populated_timestamps = - if show_activity - populate_timestamps_by_project( - commits_log, - timestamps, - raw_repository - ) - else - populate_timestamps(commits_log, timestamps) - end - timestamps.merge!(populated_timestamps) - end + def initialize(repositories, user) + @timestamps = {} + date_timestamps = [] + + repositories.select(&:exists?).reject(&:empty?).each do |raw_repository| + commits_log = raw_repository.commits_per_day_for_user(user) + date_timestamps << commits_log end - timestamps - end - def self.populate_timestamps(commits_log, timestamps) - commits_log.each do |timestamp_date, commits_count| - hash = { "#{timestamp_date}" => commits_count } - if timestamps.has_key?("#{timestamp_date}") - timestamps.merge!(hash) do |timestamp_date, commits_count, - new_commits_count| commits_count = commits_count.to_i + - new_commits_count - end - else - timestamps.merge!(hash) - end + date_timestamps = date_timestamps.inject do |collection, date| + collection.merge(date) { |k, old_v, new_v| old_v + new_v } end - timestamps - end - def self.populate_timestamps_by_project(commits_log, timestamps, - project) - commits_log.each do |timestamp_date, commits_count| - if timestamps.has_key?("#{timestamp_date}") - timestamps["#{timestamp_date}"]. - merge!(project.path_with_namespace => commits_count) - else - hash = { "#{timestamp_date}" => { project.path_with_namespace => - commits_count } } - timestamps.merge!(hash) - end + date_timestamps ||= [] + date_timestamps.each do |date, commits| + timestamp = Date.parse(date).to_time.to_i.to_s + @timestamps[timestamp] = commits end - timestamps - end - - def self.latest_commit_date(timestamps) - if timestamps.nil? || timestamps.empty? - DateTime.now.to_date - else - Time.at(timestamps.keys.first.to_i).to_date - end - end - - def self.last_commit_date(timestamps) - latest_commit_date(timestamps).to_formatted_s(:long).to_s - end - - def self.commit_activity_match(user_activities, date) - user_activities.select { |x| Time.at(x.to_i) == Time.parse(date) } end end end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index bfbe5254bb..0c537a552c 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -2,12 +2,12 @@ require 'spec_helper' describe UsersController do let(:user) { create(:user, username: "user1", name: "User 1", email: "user1@gitlab.com") } - + before do sign_in(user) end - describe "GET #show" do + describe "GET #show" do render_views before do get :show, username: user.username From c9f18d4587f16eb16b8b902d69576520c08b7f5a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 29 Jan 2015 01:03:20 -0800 Subject: [PATCH 122/124] Make sure we dont have exception on date parsing --- lib/gitlab/commits_calendar.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/commits_calendar.rb b/lib/gitlab/commits_calendar.rb index b6699c585f..ccc80d080a 100644 --- a/lib/gitlab/commits_calendar.rb +++ b/lib/gitlab/commits_calendar.rb @@ -17,8 +17,8 @@ module Gitlab date_timestamps ||= [] date_timestamps.each do |date, commits| - timestamp = Date.parse(date).to_time.to_i.to_s - @timestamps[timestamp] = commits + timestamp = Date.parse(date).to_time.to_i.to_s rescue nil + @timestamps[timestamp] = commits if timestamp end end end From 10b372397a22f3483cf860b69a7ad3ff294f4f21 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 29 Jan 2015 11:51:49 -0800 Subject: [PATCH 123/124] Update sidetiq to version 0.6.3. This also downgrades ice_cube to 0.11.1. --- Gemfile | 2 +- Gemfile.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 4129fe5764..78b052e6e0 100644 --- a/Gemfile +++ b/Gemfile @@ -119,7 +119,7 @@ gem "acts-as-taggable-on" # Background jobs gem 'slim' gem 'sinatra', require: nil -gem 'sidetiq', '0.6.1' +gem 'sidetiq', '0.6.3' gem 'sidekiq', '~> 3.3' # HTTP requests diff --git a/Gemfile.lock b/Gemfile.lock index cc3f3c60ec..cafcfda07c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -261,7 +261,7 @@ GEM httpauth (0.2.1) httpclient (2.5.3.3) i18n (0.6.11) - ice_cube (0.12.1) + ice_cube (0.11.1) ice_nine (0.10.0) jasmine (2.0.2) jasmine-core (~> 2.0.0) @@ -504,10 +504,10 @@ GEM json redis (>= 3.0.6) redis-namespace (>= 1.3.1) - sidetiq (0.6.1) + sidetiq (0.6.3) celluloid (>= 0.14.1) - ice_cube (~> 0.12.0) - sidekiq (>= 2.16.0) + ice_cube (= 0.11.1) + sidekiq (>= 3.0.0) simple_oauth (0.1.9) simplecov (0.9.0) docile (~> 1.1.0) @@ -727,7 +727,7 @@ DEPENDENCIES settingslogic shoulda-matchers (~> 2.1.0) sidekiq (~> 3.3) - sidetiq (= 0.6.1) + sidetiq (= 0.6.3) simplecov sinatra six From a538dc26a6e2a0529824d019689b615548419158 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Thu, 29 Jan 2015 11:57:22 -0800 Subject: [PATCH 124/124] Add missing end in group.rb Fix redmine issue tracker tests, fix typos. Make sure all fields exist for jira tracker. --- app/models/group.rb | 1 + app/models/project.rb | 8 ++++++++ app/models/project_services/jira_service.rb | 8 +++----- app/services/files/create_service.rb | 4 ---- spec/helpers/gitlab_markdown_helper_spec.rb | 15 +++++++++------ spec/models/jira_service_spec.rb | 2 ++ 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/app/models/group.rb b/app/models/group.rb index 9a30cf3a85..2c62fffd09 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -95,6 +95,7 @@ class Group < Namespace def ldap_synced? ldap_cn.present? + end def post_create_hook system_hook_service.execute_hooks_for(self, :create) diff --git a/app/models/project.rb b/app/models/project.rb index f124a8aef4..8176075dde 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -383,6 +383,14 @@ class Project < ActiveRecord::Base @ci_service ||= ci_services.select(&:activated?).first end + def jira_tracker? + issues_tracker.to_param == 'jira' + end + + def redmine_tracker? + issues_tracker.to_param == 'redmine' + end + def avatar_type unless self.avatar.image? self.errors.add :avatar, 'only images allowed' diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index a5c024da31..12e279f0ca 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -1,11 +1,9 @@ class JiraService < IssueTrackerService include HTTParty - prop_accessor :username, :password, :api_version, :jira_issue_transition_id + prop_accessor :username, :password, :api_version, :jira_issue_transition_id, :title, :description, :project_url, :issues_url, :new_issue_url - validates :username, :password, presence: true, if: :activated? - before_validation :set_api_version def title @@ -29,12 +27,12 @@ class JiraService < IssueTrackerService end def fields - [ + super.push( { type: 'text', name: 'username', placeholder: '' }, { type: 'password', name: 'password', placeholder: '' }, { type: 'text', name: 'api_version', placeholder: '2' }, { type: 'text', name: 'jira_issue_transition_id', placeholder: '2' } - ] + ) end def set_api_version diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 18d939862c..0a0cab85aa 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -9,10 +9,6 @@ module Files return error("You are not allowed to create file in this branch") end - unless repository.branch_names.include?(ref) - return error("You can only create files if you are on top of a branch") - end - if git_hook && !git_hook.commit_message_allowed?(params[:commit_message]) return error("Commit message must match next format: #{git_hook.commit_message_regex}") end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index fd3fe617f7..0cf9745656 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -298,7 +298,7 @@ describe GitlabMarkdownHelper do before do jira = @project.create_jira_service if @project.jira_service.nil? - properties = {"title"=>"JIRA tracker", "project_url"=>"http://jira.example/issues/?jql=project=A", "issues_url"=>"http://jira.example/browse/:id", "new_issue_url"=>"http://jira.example/secure/CreateIssue.jspa"} + properties = {"title"=>"JIRA tracker", "project_url"=>"http://jira.example/issues/?jql=project=A", "issues_url"=>"http://jira.example/browse/JIRA-1", "new_issue_url"=>"http://jira.example/secure/CreateIssue.jspa"} jira.update_attributes(properties: properties, active: true) end @@ -346,10 +346,13 @@ describe GitlabMarkdownHelper do let(:reference) { "##{issue.iid}" } before do - issue_tracker_config = { "redmine" => { "title" => "Redmine tracker", "issues_url" => "http://redmine.example/issues/:id" } } - Gitlab.config.stub(:issues_tracker).and_return(issue_tracker_config) - @project.stub(:issues_tracker).and_return("redmine") - @project.stub(:issues_tracker_id).and_return("REDMINE") + redmine = @project.create_redmine_service if @project.redmine_service.nil? + properties = {"title"=>"Redmine", "project_url"=>"http://redmine.example/projects/A", "issues_url"=>"http://redmine.example/issues/:id", "new_issue_url"=>"http://redmine.example/projects/A/issues/new"} + redmine.update_attributes(properties: properties, active: true) + end + + after do + @project.redmine_service.destroy! unless @project.redmine_service.nil? end it "should link using a valid id" do @@ -377,7 +380,7 @@ describe GitlabMarkdownHelper do end it "should include a title attribute" do - title = "Issue in Redmine tracker" + title = "Issue in Redmine" gfm(actual).should match(/title="#{title}"/) end diff --git a/spec/models/jira_service_spec.rb b/spec/models/jira_service_spec.rb index 09eca59ab3..c96f339143 100644 --- a/spec/models/jira_service_spec.rb +++ b/spec/models/jira_service_spec.rb @@ -41,6 +41,8 @@ describe JiraService do WebMock.should have_requested(:post, @api_url).with( body: /this-is-a-custom-id/ ).once + end + end describe "Validations" do context "active" do

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 delta 678 zcmV;X0$Kf{1G5E?8Gi-<006|aY&!q|00eVFNmK|32nc)#WQYI&010qNS#tmY3ljhU z3ljkVnw%H_00LD>L_t(Ijir^nYZgHeho9feOB7?_M-eTA_z$Q^ASo0gB5IXF5Yd}h zXc16Egos+$Ngxmq8x=&5IV`MY!A>6>;UFg32p%2fk*CsQ(2aEOBw|_RtW?k zVo{-&o09q?qC?W6I9k<2M0^G&B|QhGft|p9U~5FQO9}wB0OfasO<-3|c1fB6MkHN# z_n9=t%nnJq27m012#N$He3Q)Vbwu;^EvLi3h_BVr;+G+Y@2Tz@Q@bq*-OKMLRclGG{QmLwXI zG#wF_B&`MRN%{i3t%S8F+jS`z=&MfhJih{9W&^;+h`5vI`FN7NF9k1YUI95<>+6u=yLbTy2KR$ZNS5bI9xd(^NRm0$|NZw)~pz$-Hw2d>iE0Re0PP68LIpvqXlL{rjjpcmMe2r3nH0gvkh zCvilCfM1f%m-q2{T6gjBB#>&aig^<_&BE~qm;vqr?|}#I{|}fNzQN{{t8Bm%x#Tn5E`FxgTn9372#b_?y1_2bq5CC(Jb_Q2+n{ M07*qoM6N<$f(}3-_y7O^ diff --git a/app/assets/images/icon-link.png b/app/assets/images/icon-link.png index 32ade0fe9a37d114ff86789f34242a845f0c5294..60021d5ac47686cce70570d22bad6670082caf56 100644 GIT binary patch delta 703 zcmV;w0zm!y2i66UBYy&&NklgV}u2A9V`( z|CkM$_qD?LO$zV1pndso;XM_!cl{936T$T9T?p@ES0>-yuPhd9pU4A3`}i{e*r?YF z`PKBtrU3};$FaZ7dcDx5&q4v#t3Y}P?^R&E0;E@g_J0uYUWN0thtPiUvVO(?(jG$l zDwk1#_7L7X6?hNfy$YmPfb}ZS9un67)DLoI69DEzdIdJprU&}m!K$Br`!aYuq*tKD z_A2OD55XJL6M^&)WK2&4(nA(Z&jiv#hMnJ7_|vaTOd|~F@W-tPY&==qSARq((Cp7A z5Zw10Wq;1^#NA&A2b%mZW)QZuCs)r_ozC2Ru>&$`^FSYtHtF#|>Icl0w;};K=T}#- z-@lW0mLmXrseqvOFIL|@`u_9Wp#XZF5y<_ez;=ZZNKXOe;zl4n1@PFZu*TmH4q!-y z%8}Vm14+^oKm{G?8K461IY0!`6Tk|*=KvnY$vFEF6+kvs+^6#>6+odY;rz<`cM%=X lZH=}!)YLY1PX0mE{Rcx3aB6uz8Rq~1002ovPDHLkV1l{5TIv7* delta 998 zcmcb{`kQ@%ay>+sSM@po%-5E{-7) zhv#0i&kzojIq-3}jHr=|;dT|ajSFR)Lk#LgmNb`Wc(5&WWo3_+SyHlb@!A%)rY07t z=sPV}l30CJFFA>o3acn>5mLHzi7$yWGlwH%D&M1Qy~^_U_uoH_`*iI4?)U92cXoHT zO$pDeOG>H8QL8`lCTmrt-m1r`=dM|^uQHq))mXFqKv_ikw2c=eH;7*PIrS9_LzR*S zL&AHG`gaG+?%ZzD-cWr`(`K>u>%z&xcNkW^uguGR7;Kei zO#Q}i!}w50^@GE&mpOGLg22c-|v02eyYHBY-+#p_Dd2H zQ=f&+4nH~3+*U#MGf;w}U{*C*W%FE?vh=153&-QJ7{RQ*A|EE^CGM#U8NSIz- zS0VQyT%@+bgp={g-{kcw)vlTe{F`P4o#$Ng{r|-)Rc;D5^i}5bBBie~7Afp6`rB!Ggt=t@om*BLTCX)O`YrQ_aX~qY=Xr*N ze%45T=1KcBJ*v4TeB-TuxM+)d!-f3VwbqC1Gv6~L$zNJr`YZXsUS5a2=Qt+GF&FrA zZ`#LtqDsl*x0=Ei`;*+3RSCHwOZT#d96t13T-|-e_m|uCPfosck15INJjWTKUu+-P z_S8S>s%7qQvejX*z2vp&xB0p&%DMOC^gk|Uxn6x;fW_y*#}yXQ-!9g^cdut*Qm)v? z#IyhYtBs$7zt<~DNE{1a)BO0;TOzHM5k~}l)e_ZtQzI8DjPVDCrGhr#bZI@eDXx^rHh#e=2wUggFmyWX5!t$Q`re{YPu zbPm70{VfKK^h>on7F;o|H_!@>v;KZwcGcHA^-H4CYBsE|&oKHOZs`6(!dajoYu5KE zGQec7TH+c}l9E`GYL#4+3Zxi}3=BNstx6`DrEPiAAXljw$&`sS2LCiRr09sfj6-g(p)%8I!@& L)z4*}Q$iB}08gz9 diff --git a/app/assets/images/icon-search.png b/app/assets/images/icon-search.png index 084b89e3a7cee5787bc3fae15596dab09c547cd7..3c1c146541d456a042db5768154a307b9b535e9d 100644 GIT binary patch delta 137 zcmV;40CxY&0^R|TB$1Ijf8w_fWWJaDu%YkOxpw}tV`T2p#MDu0n%=zVXlNT)=%(V> zLMOj8j9sSU*0c3285pJF%!gK%+`CT2wz;8ZV$F%7pPEBo#%??)iiuuox-LAqa_U_% rv!7KCOcgV`S*7RGovr+5&t~!sY?VjFX3{3I00000NkvXXu0mjfq+>_7 delta 244 zcmcb|c$#T~N)}VGlV=DAN9Y?j76t|e&H|6fVg?3oArNM~bhqvgP*A4CHKHUqKdq!Z zu_%?nF(p4KRlzN@D78GlD7#p}IoPyt>ti#ZIx&zs=c3falFa-(g^ePaWC zL&M*v=lqGEb@_F7#(7hl$M#&*^WD=**6v&Ch3bD6pChR8&}crv|h zmdKI;Vst0GmE!SO5S3 diff --git a/app/assets/images/icon_sprite.png b/app/assets/images/icon_sprite.png index 9ad65fc443bdbbc50fa25797bced0680c8135b5c..2e7a5023398e7aa1d2794755af4f90d59b431919 100644 GIT binary patch literal 2636 zcmY*bX*e5L7mjAo+SO=@Ce*&~N-Km~YO5HvYi+d~6Gg3IqOG-vUF;pi(pamuT5HnS z877p`qA0b71c}-vX6F0x{W#}7&wbB5=RNN^KW?Ih`8{?P5DNeRU^h0>x1^s!bVFTc zqI+LYKM(-G!egwjYZdm_cHR{W>owkf=R@-hPq6PC=80SBg!fgVxj0Wzi{6~w^OUNh z#)?!?1A`ybxt=Tp=%!40gF~MPB(OZSZD3TbYYWy%HrEeezAh^)1ih?v8vPN89`1my zFQkcC0fYlWet<$2PBerg{iat*J7?uradu*|S@0GbG2H>8{gXcXTM(kRV3 zlrHktS2l$|8|E&?wokQ%Qbk3&tIK-Bb( zdSk|x7F~{Coe!*yAM|Z~5n5DyvJlHN9#_TQFfXq-aiI*;CC`Mh~ zOUr*8dT;JW0nZ&|djz8=u&4&gglx#tQ-5Bx>ksh zB;$zToM+aywx&Kl)j#s`4vrS6f1R#%%-HW^ee-!7p4xqo(@W&oHQYE`Y;zCnrcfM6jl0?Im;@@eADkQrXcD)>|enuTF4}@7+E-oO^SC&0Xn>Weq$c zdq_!2Dl*NK=96gV{G$b|X4?R{(Cu^4!?<9M+Y;Mn7!m(SkKqDc32$?TY1%_FDbmnI zwa13HVN#|($Nh_Wp%F>b>QXcF*ZFZqe;{k3*DVF?84>WvjoH|uE&d&m9 zBCJ`)FCytvW3k>UFfUpec&RXy9+97$m1Sga&#$DH=WB%di^1vs9WE|w(H=g#Ds0@K z`eGz?r;%WLPch%QG^r?ZTWv7JuN!`v>TM3zH*W#`SP_CMk+{K@AfL?{m(xa+D!0(9 z*_+}{UhP@D)=5q(eDHx&YND{@q%Ml6bWEp4Gd@X`|D9xWrd(k;LMHt>k=Aj3x|?Cb zH`-qvZ&F-RLU|*dtbDYdF%`PgzE-Hn;g3F{QMh57nBqB)uyBuFJSaTv4zAD>JQ?Jw z3Nz8N`3O6(ayp}oYu4kKI4!$ND}+GJF7Ze*vwJLR&Tcp2; zIofSBU+www(vthmr*`FE&yeQ_2rxZ948)Km2 zHD<=f$(frja&Jhonesj`FwlJ{ODfL1L|g9P5D(O~;amj;i&!aXzmIjn0e?UQMG^56Zx`0_!4!Kr9; zy}6N5TCHR2(Bbx+k6UL_b0gti@{R0aYN*E(Q z8~|%YynQbK6qS(3{l?T0vaKAVFq5a^#YU&T&fvNa*cgASAEgWmKzv>L@bVd0Gh~D- z?qZaE-x%)PfQIc_Sz2aneQJNu^7E|l%n@bH;4{d*M#*g@jTQKks8cBpFP2sD99iUf zw*#Ys8VMJABzH5)5#w%}^VUxQs}P#hLQy=Z#19bY>XIIDO4!4fYvkr1kE$2LlkAp7 z3_02rzncz}SgIdLp@YJGdZXrB{0p|`y9EB*4JI2(US3{*d*^p)Jb^&o*RR=IU%gwm z%8!aS4#FxPROS>F7dvHUWC&MRS3iA$o|(eg!qZuSeydYdF-NT$-!KO+_1x3(+L6O`~$XbF^@0;XiW9aJx*3;oG!DRVN0$q)w*(=oljw}ScP(L7xQN*7&(waA5V=W%K` znWL4W6>rFauFI5{SiZs|z9`~(dm(bs)#V^@MuZfgxsW-+os^lZ&EqQwlUs{~Lt#q3 z7Q;LjNKo<;BoDnt%fn#4BGGmQ;eqykW!Kb3p<3(PbQo9w(<1ofjEX7^A3+*#QC9j~ zR&g$?>&A%q<0A;h`1Ztnd(b`3SL2~M6s^GJ#C3)$T{&NeaHas*LM9$kEZW_?(d+~1 z4eV|u*D+i)>^nWI`Du*$4ggqik>_<5(n5Vjl49axuuRQRG6%gec@$tTkHv0`3JGJ~ zObn$(bgNVFprv)!)hOtKBNey;MYU3a)bP1zu4(8ic%?Y_)d$4!ybqC!B_At#@U2Ci z(G{eo?{n1k<h1v_ zTC#p!U0+S%M{!!N2a87zZgXayw%&&=LF9je}K6CzzXg80CP9U18~J1jq(8B@Vk4@Fbld7$Myc|= zi1G$E?!>BXcNFIQ=`p7f+YfKiAY_@8h|7kJ$;_i3na5iyDMz$$&&wSR4Z`HbfG7Yq} zXsmd{;pY$Al8lVsdxwW}CYsRwg9VqCK36+t?0xmAuMG9mW&PsT4^SZ)AbuH zJ7nI+7Uq%r^xf#*p`k{)^F-7~FsoRqJqD^2(126PTD$30u}4IR>i%dKs-H`}wAc|Z z8nnys=7|sm5&mp~I}pY-71#0t-$%EqH(N^*IaAFTNI}=MUWhv>85bef>X(y#X0U^}`2MoN zwalS=Rfp1#`{)a&^~mtLXOy+Z&4w(^uGaQnIY(hK7=INPw=rVh-&`~T@}bo>#S)t# zmp7lU!ijX>j2VQ|tt^96Yb7zsIT|HfzH9P?0EXPSjn`TFFUTB_7wd_|W8d1F6Q=6;^v4{4dlDSu?u ze{i6T9Wn2JR3>=Tx__k)=rYh3y=}t{u_-?5PMGg^2~+YZCLJ5+w7<>qyK>b{`j&KB zwg3t>=fyYT?Y4XKlr^<7pi2HXDWISKwNI@exr(|d?#|bXo&&ks;f%$Ut%dF!{w^wA z(&}2r34(3)E0CO=K15GX@1&+?{ju|1e-0O-Gki3hTXcN;@am*YRkPo1tjDPQZoZrZ zrOzf?`EK25Vpf!(=8#oYg9ugtpEkBL7i%W&(CBMF+}R1w(+hcJS_E_Q9LzsOyf@s~ zCMyR+CB(ZOlelH^N)1suRzoG&om-x2ZelHucK_?vmRa2$y2sfnKOKGn@ogaQq$p4W zf664=vgZJoH%gk*QMi}NVzJ6sHj>moHlbaRPaNkL7EUQCwg2luM*Pm9mIf3h>C+0R zza^XrH$eGZYJRYsQRUb)#7~d|9&iu5IGF50&w#m%ud zXUKSCgP_Ih$1k8BZTDQ%)O?0L=*ZQoh!Zl3eAPJ`P7WD(yo#K*Z@O85wmpKHv`zV^ zj5f?tILV>i4r8O~OFfUg`0R+PTUSb*b8M@u+k4}qbq^1V)pN3Ocsw5JOkH_nYXX!M)`RPS@Fs5E<_r>b0mdrl~Qzr783+D4*DvSCi7bhdZsm#_iF1s`ep3N z(LWc66T8e(=TyP2JO7+mkUn(gikfliiBnRlrmr_F$uZMfAD95e3!NP!wZ+f%&a1py zeBl5f7Gj19c>-$ypys~csA!yl+nQ!DE5c_6!Zr@%#VZ7FzsIh!`tSZSsIv(D`6d6! zKPDRei2qYc8Tro?2}Kwd-%Tj$cpJD-^-1ddXN<+sx_or-S=A8f(oc4flMqPtf3&o0 zLf5Es{JxwT=XZT&$)N$>xmYb1%`ucspLZ|Ak6#Y$PGv%%8Z7h;oy zybe8~6n8;;iCd7g{4F8GK1s`^5PK_h{cDJ0l#L>CtnR_(j&iI5HfIn%*DX@?$5G_% zlGpEJrNt-I(sIG*0;C5=VWQFq65j;6*pe#HF78;(XmpF&wTV`s=eWF}#rDw&Yfwx3 z=&WZ#&;Q&ERKT@)mSMV9$7}mnJ`o^tFf@%1jnxU*7GFu`q2Sz8z;v9+1cw!>K<}bu zCd=y;U25$P$9d!|?R2He4Xxv?B=wbG=V-C6U8+2x{ZIz#n!&Ux*Lj`?)WpiwaneZc z$EM;Qm9r;WMT!sgwyXZ6k-YJIZN;Cw~qzHoP5AQ6pI?%kk-~>=Q z*wgRs-I8Pgj}fL-29P7asQmIEwxJrX?Wi*;=!I6SDc{DwbbGI8xL}_!aU|)hot~=n z5QP9*b8yrafd$3YLSb6Zm%@wXE_l#)L$nz7g+g(Av*UX+1`?c!8N208fvn|C!Wuia zZ-;He;I@sj4h-(g4556)TOs77$vr5R6EI~N!D6>^0%ljzsKA+T(=$@0q6oFJNF%?6 z(RXAvg=WRhU1U(2NNjckvcbYLGOch(rYJH-;vnxgfT`)s(M7A#aE0^S$ewRg!6Sn~ zxnnRQSt!K}Ru}Lr=2Re5Fw??}7T&hDsZ@qf+nIhBY0mdIIe2q3R=?;r1b2`;@Ye#9 z$4jd7_;au)guGqAKgo?mV<7+y-5!KF8e0$K#u(1$gQpe zTAn^)k_CBf2V^m{AM+E>8_Ab;IeiOOWdlqc`R6-sI4)O{6U#It(PF*#A0iA5vr7E0 z)3QIEE}pJ!o+Km)qx;m<6M&PI-<@2Xkj=Q!SaZvK> zm;)Bk$G>j5a(C7#AZfD-Br@&sob&Wgo5BuqeE}tAy!D|9+I&gq;qXsM*M#qD!)Gc& zE|Xm!%OLaS%K;x6t#V9$4aGWya^H_>JT*wxF8_71e};$n|Hav|Xo$8j3f26zLci~! zTB0#Dde+Ad>p2s5nVkj+I96-pBs%huktCEokNbZEKvz^u*=8pKv%`5GR=^E}IlRWm GHSWLDaEBQH diff --git a/app/assets/images/images.png b/app/assets/images/images.png index da91f6b1f4c31422a3890c3ad8a38bb418a1b81c..ad146246caf907144b468a121ef9524ba8ec3c74 100644 GIT binary patch literal 5849 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 literal 6644 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 diff --git a/app/assets/images/logo-black.png b/app/assets/images/logo-black.png index 4a96572d570108366da2cf5aa8213f69b591a2a3..49cdc16cacd9a6be58ad79813461554bed5db8e2 100644 GIT binary patch delta 2600 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 KMNUMnLSTYdc-}hz delta 2790 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 diff --git a/app/assets/images/logo-white.png b/app/assets/images/logo-white.png index bc2ef601a538d69ef99d5bdafa605e63f902e8e4..2299153caba9c4e96ecfbc1c6e60543c79085c38 100644 GIT binary patch literal 7331 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 XBYOdNmDsD+00000NkvXXu0mjf1LwCf diff --git a/app/assets/images/monokai-scheme-preview.png b/app/assets/images/monokai-scheme-preview.png index 6791d1ee33dfbba35b14ad179e13ac127ab99421..fbb339c6a9170de79c5fee8fc2a5abd6a785f4e7 100644 GIT binary patch literal 3711 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 literal 5401 zcmY*dbyU>d)Bl3Rl7ckSA<_-fpmazJEVTklic1PetqAf6EG^yAES(F|AtAN2bV#Rk zzdXP9kKg$ms|1 z$o-|70SE+|UDjTDP)R(Lj6L*WwjN$@-JpQJy{Cr<)Xh3*1P=hnYt=$%}EobQ&JTH&5Fc+ z%v$^$8{> zsrb03>-~_1%m)A-Z78|=+d%VnATih=d$S%a;eh*r8>$Y~dp zZr{DkvuHKI2eJ&xEZH6XZau&qjAtJ>6Y@zLe zykTkAt2HL#M8)2jx<|-I0M2v*on1Jd-wxhVk5ct_lx!zUpxq{Zg$`eFTb!QLxvz~!jB2Mp+pJUS2t>r{t1EKBd5jWKkn(>84wr5PrWC zaHYxB0+9k^n!$+tcZC+tIM6cD$3wFW3@d{e_wK*Ful*hap6GGuN^)95d~e^tJhN z5;a8IGpEadO}7F;f75%DrQ^+?E5GCcIBlf8+K;N*YL$F{f6QD~9lwtit*jASohwL{ zwkA5nNmY{H-qu#Ci{zI%SipUJ?SwY`n_@jF*CL=o2pO-Nniys_ei|+o zJ)E3D9-Kk>}8zml8XA(3lUWW$ag?zY2q z+u#RIs+);pRGuTEbK?hASfoGUHDDN|G(DGHcT)+^9`pP|>63J(R7r=sH5S_oSwKG? z89o!LUpF&ko4|gwWNa40!$%9i8J70=0N^aB$yffwEX+9a5mwmxyTnp(#O34|4Di&@ z{slwnq?%d0A6J9n6`qxH!anHS70?@@1nkwk-=sL=U=R<~e(~~6ZG1afm-$XRdOj?| ze&g$2AhM5%*Fg1wV#r2)FH$f_Gd_Pf25SId|8R}r{?4^N9UMJ59w zWW#r7tLI}R4*a1<@sQI*s!`ur;kusyJ1&=KeLk>R1WtIGUF(-w*+3O-%|hHoUwhIF zUen;DSFmc4Ikt;Qu$-Byd|R_TpGB>uq<`DIv2@42&8&YAHUY~1zmRhA_DRIBZ}Hk^ zj?W|Zl~%prd8y1_QsZI=I`dJMBas=MVKdj>>bn=KHltOyRT3#h>WB80aq*QVsGcr+ z(I5pUU(dG+>B-5t_aQ4^4NDlrTW>?fUsqd4dLG#!riSkzJkxUR!Lno2KMi0O3lQZ> zlh&a@6Rj4obMlL@BfEOV+}6Tq{j=(ghE#MGyEZgWD^;OQC#u%X?w(4F$75jElE3;^RxWwq`zLXPu^~I#0{W`#>8kb8c0E@<`<>-!uQjee z%9nsnK&+xOtbf+jOB`<#| z@}pw8_&&-Su9US_+6^M=0!B|_sy;;p?E|wHq>Ra&x+^3ZVdTyyxmI3j=a5{D-z3n| z>joI6jK>I)LnrDKiQTque>GU2!4?o5kn&>#rnO-VoP{qMht?`&pRdxCJ-lvAY0Ne9 z-73ZsT6apRY0A%^(t|#8tN`bbeHKc9X^v=JEkMgq13=L#;_~T4DYg$0*=7dDD|OON zmke&9nvF-#jKbC`WA5k0gu)nznSkAMzgj7cVk?Pfu17;Li;d>MQhgQy(5x~)e9*Ob zJugP3$r*%q`{f3S@`uVT&b>`FuJ!I2Vy^Agzl-N)2LW)e>4xmOeen+g$MC5bsJA#@ ztk0ttn$K{7du(&M)O0SI_wL+pa$LyV-`W>9*ym~u4JT_*-u>{%nZX7cenahSi>zw^ z^_7r3eC;InTI&%~30BugYI$j=y)kSU_ala16SxiBe51R@guqM6)iD{%5kE=~jDQ+N z$=P<11VKc44t8o6y2z^ixk!LXJh+4()<=!k_*YM?UXLs{v1A9vn0~!+%J|FlmA~)V zW`#Aw5>S?*kr!%XGJ8@2qOKxjSb#pgW6%%lJgljY>F=Br{#)8ME0qSl_9yamDUh{_ z6PKxjk_A={`n#&~v}2h0`3jOnvkw|C^KPc#0v&7`7Hh-1HuOLw*Ogx-&ygj}@8*(? z#klYpUNX;r%Zmg=$wb1lG|Ea{MCNujK?i#;VS$>>GFmt?Wf5!5pUYBObk}1M3tYsZ zPuX^2rE#8F5q1-L@_hpIyrifSxs;t-)AL10_S|SEtBmPSEnn_pbn0o(QmSbx^1Q{t z!*Po&F0C2PE_dho0U%DZ9+~mlUr{-@v&{6Lqtx6K7CNb`Ite+Z;$8f$&Sr~Dji062 zihaYKijPm*_#Aj@Qdy6|-s6ZdekAedbdY!O^pC_*F==;<7-#E`mORv@x4+N(AX)^- zePAa_T)ckVXr04}6d<{pEB=d+GrN5O;UL& z`sXR3RDYCsJzY7c;N}QDRq>2ngkfZ1%8#b1mOnP`172kT8hU=&mZ7MP4`g^6B_q2xmAY zFaJA38aEbf#qW@gCwmcIl#16Rah4E zC){Gl$%w}7&0^Y^!NuPkr6TB?Ww>(Hka zSRvF-+Q;KrN+>ngHJH9>PC3M&vimv!rAfn7&IhVeJVlnjEs4f~u$DY^-fqz0#Glx~ zI}Y_^!TV%bu>WQV4kYCQvcDr`U8F5pzIelCoKx^64LKEst^!xg`TGA5& zw04}=JTfIpL0}@L-tX?_5B4+j$$d2%R}w6RS%Z%bzUECtz>KCX2qP?pE00jP3NEs0 z!tfG79ro8FNkEJnJ2>_bl=48wQgyzQ}^s2@+<;BYv&b_^o{gwer!^Q50wx3#J~4Gc`V zh;+B{tD&(yqg-qg&!}^@dh(}H2G^m)^oVf_D%C>JFh_qs6FoCF1c=tt0-)dDlSSe= z?I@rRm6dT=8pFUUbhNP_H&lZ4@I{m`L{(?N_4ym~Ghq&_JNSK)Kwdy6zt++&p-tRNz) zN}Bz-^v&&lieGvko#R}#Q~5LJNj`$_8evl`I@@v0Jsl@8Y#;;tR~hN1yzPbokk@#Yfa<1cL6JQQ&+W+9=+?= zA!6bM#Y$Shgv#XpytZCkG=!iJIc5@&@i*2EkL!oqSo>$2mdcxRlXz57_5fvQE}%Je z_hWnRhF*mb%UQTSfl>^Nk>wO<(ft%96-f=s;@O)}!fkp0rJnH-NZ)XgbaLnPFT{N< zYMk5l`Ijn@@qnKv)FofT7X8^&h{W_gElNW$1Qyfc@_FbfGIS=)Or2Y5GbZxRH#lv4 z#*zNc)n@V4<(+yr`y&E?$N9l6g2#Qfm5p>xXL!6EmwWYCgU~=}Vc`uo>9=@?5i!aH z*^1J_*S-9Gin!vcT9o}##O1$Zd*{7aNR{CquZ=$_7#mt zBirJ><~*&sqH5_5GWwT%VEF%G;6K}5NPfi*I>16dOQeI9^`r3y#3%%9kCWfm3hFxg zQ%fM5z@IQO&&Q=4hR71&UD)^%r+dqLfp@u~=TmAt5IV|;9HFX{Jzu%q)I$+O`5Q=- z99~$B-)#?Cjy&_#*K%!w!?6cDX)NwlcOgR}CC`M==#b+n{9n^$^s>7y0!Y;05Q&h) z-}wh4O)A8=T93_or+6w-zL3L=SFW@TXcMr&ry;ig&n@qQ46lM4Ha9O;)mVf6E*972 zuX}3#nxFRK7AQLvL>qBb!Yj>dYL_4SmW;Q9`}YaG)Vsfml>~?W;7|S^o)A#O$Zso_ zswt*9d}O7*V$c1U!JEis{Fw9V|Ayq>p#%x&PN6+8=aB=^L89H9G>y|Q>q;z5!Jtn% z%ubypTT13H@9tteM)f$Ep~#><*$zr1n+KjDFAYHWkZK7n`U<~;v3)&2`C#!@k;P-( zqQ%1jdWmH7E$m##Xp|MXGwY5cuWE@Q{?6$pNkmQG!Bo7j1yjyANBN|XZMC6sxynx^ z;e;<<#hxe$LtZY$$6!gWx_yd{ElyB21JB&&|8?dEs}z1d zVAa835~SjPNbeBmemvD(jMwwqBEe_`;zK_4`0mr&8mgcjMAH;0(S2>sWRCFeQ2#%$ zKz9~Pl2fLk$6o~cgjiFGFwRBy{V3*7P#YCGJP?elsJ$oOf?ADNktG`@|9`-tkK6Tw zSKn~{g>9q5)rw(vEPwSDK_sWT0BL$|f$6bh({r5OFrz8$3R8xyD;dG(mM9J(oU(S+ zvBP9@n|%rI52hzqyk{AE4_}%V)oUh~*=!m+%L7SQpA!?$qK4f~giPiU+Fsg+jNzqF zA^`lfb!?DaJzXvsZR8QDMu^Q;hRi(A>NH8{{KQZ?5G{LL>+S-oX|KpH&bF7A=^nuWDSo#PU{aHe8dMgNyS~(QR(kgRgb?+12+uOj!#ln1`@N2t8 z-|2|ty&7BSI&2*w7x?^R5GzEN&gHa_Oq)o|F0J@(N);b1vwX1Rc*^;<1{)c)cUau^ zgNK%}%mqd|+Q|RsD8#E4JPDGSo^EmCvvC~-fD-`52u=K#(N=C0LN3kxJ}`ORVOHFx znKOS?ME=3_3Q#4_{`6L=ZM+r&nCg!%{9|VA=aNHw(IOHLt jZK|;TQ*@MvE{FG`mgtNjsL;cF3joxVUMrTpgarK$gQ`}q diff --git a/app/assets/images/move.png b/app/assets/images/move.png index 9d2d55ddf0b460d8effa80bca82340f5a6ca3341..6a0567f8f2534837e7280dd41e4bf4b98725a3bf 100644 GIT binary patch delta 180 zcmV;l089Ub0>uH48Gi%-007x@vVQ;o0E|gQK~#7F)s?XgLm>=A*?^H4^+sZ(#Dz~N zvZ7$UCf~23cqf4Yj~}G~l*TI%&=UbY5zrI0brP`>t6sOu0j|Ff&~<>mo&0S;>4+QG z*_x`}3vh(>LX|4A-VZRM0h?`MkNJ5D>rmgv;pn7|p_7XgIxV@%)=SN&KW7#IK2`9o i^TEPL8y~KCyuSf?G5cR1sCvx+0000eSaefwW^{L9a%BKPWN%_+AW3auXJt}l zVPtu6$z?nM005>*L_t(o!|j*R3BVu>MBTuVI%-DhNEX5NBh*CE%Ry-%Uq#*n&C#T^ zR??bM6m3}O_NsTzgh!5H7akErE*u%f!|=QEW&voj t{Q@BZ^qY@-HxdHA$3Duf?u~e`(M2f_0#|W002ovPDHLkV1hDPWy}Bo diff --git a/app/assets/images/no_avatar.png b/app/assets/images/no_avatar.png index dac3ab1bb890ee2b583ac5eaa060d972b6e35867..8287acbce13e32d0823c8f5fd449099c1c61d6cd 100644 GIT binary patch delta 597 zcmV-b0;>JM1?>coBYy%kNklnYL|XX%)w zah8f%3TK^T)){A=j*(6%%O0ayjAahd3~HB%Q3Yxj$fyM<>-Zk&fU~rWSQ=*;A%h8n zvmj;x(t4NSu$C%np7?7eM) zU>$ef3W`}0 zs1~Ss_PdnL&p@`|@W!&C^z8s7t9dM&&eMl9pQ_=6v1-98xOXk4-zKx!>(@7F;9kYV z<>Qv#Gsd!dm{>`v+u<`AV{ETKWLdps_x!)Sju?*jkW9HfF*K}*$J8xWlC$|E}O6gYWT6|58yH-KB3fC&>R_R*xaz$)eyOv#0 zt%Y?hSWYd8tzl#_s>R0Ej%w}3mRt~%W2>3csA)5{AeoY=1U!~mzD#CwRI|y;(m2*5 jmCRmI%@XzR|G)YJi=(k0Tjf%>00000NkvXXu0mjfTK_cd delta 681 zcmV;a0#^O)1i%H5BYyw^b5ch_0Itp)=>Px#32;bRa{vGf6951U69E94oEQKA00(qQ zO+^RX1{xA73;x$u5dZ)H0%A)?L;(MXkIcUS00KlwL_t(o!`0XAisCR7fZ@6Ss~FgY zu~txYZBtUw&`tmn-9+4pbcX&ahDeSa}u*USHFsiAEywS)Sk zr8`;Qlf!4Mt`-l;4r}R?7j<;TL!c65=|eCLvD7{o=sao`q6*M`86LD? zdw+NjvQARJ7huU#Jk_(P)iivN~m?-`6$+cXY|HrZQ%3X`-lu~ zRm{^KZuPmF+DW)&JXWp{7TY=VZGF%>Y@HPttI3^Kg|SSwaYTERwRKtsWfi%jEzYX5 zF-1$9W!7VBrH~dU)QT{JwA9?T<_c>WG0Xmzvf0s~Eq}A0lI&hO-5Zp38=fMXr8j97 ziHD_Ut%i}?m*lyKzE9%#?c0>!_NpfA^t>f^gpln1iQCqkfBbuCvdf}th&e1RH{@6D zlh@PVt9h;eWobdvKjpn^+TNGxQH3m3eMpzG(UZvRVZBsdOVY%OKipc+?R8RVr!}x- zT*~U(wSQI3QpXSS&SACL+7r!-BB#F9RF15UtdS+svJ_pTBkIu_R(NYQSkJipfo^H< zSBke%+oCIU)V2z|RoGUAw<_Ce7E5G{#|C>eH1PAnhlqzkJ0G&cq>~LP_)g(SrT*EO zf`dADaUtTMZGSsz|7Ta;p)7DA5xQJ#{lAmbLse!%>X7Y(tfDv}Lc*9;t0X8&JV+h3 zWNltQBm_vnbDMNcvf;7Bf>b;gyCS8;vmjCB7$_+mh^QPRB|8U_@zmD8RdaYqD3ERF z%&*#r$qdLvSPyNuFESu`+!3vw2R#E4^#_(dkNYkH#DC+y#MrJZ;~^JeK%Rd!jfWJ% zfIOe>`*=tq49N4=QWy`}aa-i<>HG1JfZJOCSrzwO zJJ)A}D7tD*DTbh!?iD3EL{Y0Hr9F7y?4(DLjsqd&%`b?Y8h#1nu)k@fB+mXeo>BsT zVKv2o_n|78-GX!-JyrrICAGh=5`oBnu&uCX^#Ho zo(V*Z9Bhbe{BJ@{AW`j*J~QN`Up`DA`V&3%xgi@=x!mW5oF>y1A0p1?G^ax%G)3u? zLqxv~H#a0fQSS3YoY8c}h7_ymiVcaj4j#nWI%trJr&?bElI-RyE+p`86Kx>+2hVdu z8h>tc*D8==d|=Ta33oYJ2C|bEwq}rkyUIEckGo_Y2$-H8(0W}$o&S_1=2!d2Lb|;ARyyFKtS#U0s;a80s;bJG=hMDfPjF2 zfPjF2fPjF2fPjF2fPjF2fOxDxKtMo1Kpa3oKtMo1o>YwrKWlr&Sr7mK002ovPDHLk FV1m3;qptt} literal 4884 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 diff --git a/app/assets/images/slider_handles.png b/app/assets/images/slider_handles.png index a6d477033fa5436fbad5409adcb549ac8d8f01e0..884378ec96a20c1cb2a419e1dc1aa68ab9d85c15 100644 GIT binary patch delta 1359 zcmV-V1+e;>AmIv-BYy=bNkl&c3y}rI<`ge0*CJL* z!&_jXq_d3;P2^&T;>IPKQYmnBf-;rc5o}y61Yv=M*8u{DOCd)tf)bYVHZK*mJ$;|E z=P=>d4>rp6~Ddy}$Q;fA18vR?u?W3fO`fz<&Waf!Sgmq~BKZkgr71 zA<5H3;AP+imVxEKpRM>_5_gb%tMSBbfV~uXgS8+6d<~9)JhtLTNZecU?WJBRE`b+- zJMaTLfgZGjKLME$zg^0vE8s1V0IugY^wZwri&S#8n+(7GL6-)e(z(7TbhhU)TlTr11QYIwS2r)4<})DVbIm z)@5-!un_D4{YPp$D6^`GN;~gSNypuB;k=_&jdbMvH5NC@2c8eKEba(=Wv*!-UNh&) zb^5mL*I|zS*iJs{L+K^2C2ZLr5kFbi&2{F{>%cp#%i_*p703s)JFA?s>-+Jcei`QN zpipx24Syt$m77iW&2=Fo;@Ne56q|k4EUpLstjpqaz=xm+P-I3iWkLK|JpF=c!NzD> z91>@;uPqN65kCR(s3Rq2@j}^LFOD8Ll8Pz)d?#gI?jB~v_9T<=($FtV_PuS1BjV|` z*C^^}iDKl6Ip338Ny(-4bhx&281`4i9%Ny*|9=`sonp>Mua=vs6-^XYM?8n<2LBb3ez;53xj8 z7&wI~9f=>b^fj zjkkZN+M7@5Z0}>X?3?;I&Ux<#hkB0pfT_TCrjxUCY*yJ%l`Xw=>E`1xuNE9HYk%yb zotY(7sC5K3YTv*b*ngU!i_ESuP?>RJR}0R|s%fUp32EC=Z>KhTTQEEP;K_^nx-JW@ z763OduH$;qQMBp6B`Tx%!DOx$sJqMcqN8Y9;5ON-MMQehU#OoeI*Mio+_$FYJ)F$d zVoOTyebmom9o3P460d8R%+-SH)PF}VbMCt_EO|0l3!e>Jlho&2R*TIv&5{jqx*L{U zEdrwV)uYy1eYMCs2X3zOmu^}gf9SzPt`?j(XRb#e>Ri>=x$IEj1m2kSiffR6SbXDH zR|}3K?<3@UDt4&a*&?s^I(T=1-@23!!s1#3V^bdS)q*Yi+|O|lo@L45rAU#HxPpZu z;ty7{6@Q_`9nHd)p67NlDi`1e+{L;`znw+rxwO%LqUk^@Vp{P<6nXze`v;0qoMi-I RvvL3c002ovPDHLkV1i6$w%z~$ literal 4122 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 Y50zi;R|_Mig#Z8m07*qoM6N<$g8P}p^#A|> diff --git a/app/assets/images/solarized-dark-scheme-preview.png b/app/assets/images/solarized-dark-scheme-preview.png index 8f904405310f12370147f2b231cc63e60097012e..7ed7336896b1daa3d06b3f304931bc2006a3b145 100644 GIT binary patch delta 3191 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 literal 4993 zcmV-{6MpQ8P)003kN0ssI2j?}E!00003b3#c}2nYz< z;ZNWI000nlMObuGZ)S9NVRB^vQ)qQ`bY*g5g3t*7023WaL_t(|+U;F^P!wmHe`dO; zXL>%FFBrr@L2*FAAS4)s14F=wUT~vHFxjLw$!^x&%l(ndU20Q%x!T*hyRB?xZ{uZ? z%4sgTR&up+SDLIbYJ{+26eG$(34$ZkOkkW380MpAdZwp)x@Yc>Vbs+bz(mO4_U|uv ztLc7tfBih~^S-}+-d6x?s^Bg`ot5SD`+-L$LCALjPcRRjU>-cdJa~e6@C5T%j09(u zUVUv>-|M?BwdTh%ySs3_`lT!tn(?&WuybZxvFw84rkeYfI!^w3=L9=%=J*F!y)np? zZ5vrVzyI|EOSV3XV-RNu#yd##9tjfE9o|BP$)yWHko*B%b+cTf5-?NL=n?n8W$v5W znVw+fnx8&#AMUk}5_I$~ny&C|X>hF8MNt|bI=}qLh&bJal04Urx=Eun#s-w#UoP)< z2>}2Uo*hljA~nl~rI*ewKRJ%2ZA=NyGWqfDAY;H+@60xsf|v3N`s|(E8~^|+ zDtpy{w_WQM={Gmi<|%pEIx#|o2KG*vU5R_@Yg^|4q3Ad5>BzO=?xxkkD`sf~*pj9(dsPvL zr)*61bWxsSWj-_d6~mD1?mOiGmxJ|cAX(3B@UI))l^$uEBCy!e^l>H>gx&?72&L^RnGp<{p)X{$=8{^Jo^ zBVFAf7j`*M?)SU3s>+RmYuz+w%zEf)e0=}4Q*8uMsI9u6wf06irL6X8$>{H{o%w`t ztFtOf8SCYks!_Ls_H>!3VjVYjA}U}dmKEbShaMF1jG*#|ya)$L2?YIyhIHEkmpfY51|QWz~k06-X_LA{J9P{-J* z>;s3F*tm3!vx1y0D$B*;#4>3WdQ&HI!0e~aI181Wg#N7QfCq=PaL-K;=3n)UPEIPMwk2RLs zHkF0+QWizytT!i-5?W6$Kf8XSVXIROlB?&J9T^d)X`Cwrh*H22$=(dJ93Z$~`1J;tsXLgaakex>tPyeMsTdcCBgSMLlcE@KR5<=;jkAI3^Sc`L zBU=iLB+P~U8*eL>}ZlOSy@VnQ*X zZ*qlrDmyc8niNos(4o4rkOTnLxxfFEd4oEgud^ti?f?o{sG^*dpixm`2;X+K&cFgd zB!H2$fKAspYqqfZ*(}WBw+Db6BsrtnR44=x8XA?ji}X@J4wB4tqd?QRq~+D*)F>JU zrTErFnx)O%F@p*0UG#PG5S7$aiU0`p8x**QA}OHEA*N81c#x((XY=zi8~`z9HNC9X z;{XsNoovGCiM}9Z(3F)U0Du)LYhN|2)TXvGEpFH;NGlgAi+}n|?ed#EJUDXfjpL3P zx^GT$VKR#bT6(qDcx)I4o$V*vrm}84NZ=nR`p<5(tkLrDG8zJ^FaD(0KV(_U9@8p zU2hQt3y4X-xrI6N7D2E#+bQ7n;BTKgN~>SiAxEyA>7O1WZ|?W+8YuAiHbB7rn8$o%foa8tJICx|!#A{%Y}xs>YD{Zq(oY7|p2r@w zY^!$ER!l7;vj)6K`r8>)n}4RJz9NHjo5LWc>_=Zf?A{Z7`Ur|3%DL>B@C2-~GSxIlNyk;>14@e=#1Mo?s-WxqfBz;;~;`@r!aFd~wUe zHRGS04J@2x@_fgRx~W_gagM2309T?_;cIDftWrlIEblqJ{InfKb&elyo6w^GF!B$p z0YEf-boC*7%Ap4pSN%M1rA`3hP}eVAc52!|ZGHr-BnH4z(Y#V4GkWD~YN0|j65818#4niM z>&%(Ra@8z_v0<&9qw2Pd$YG3Lf3%7mT(DWut+RBJs^t!MC`hr zaCoi$ZY&?&E~(;$Vz3e89sr6qw^NA)=hT8OaN~zO!;l&sbWe2gp*%|urUq* zAf2EK5TMLvZBteB+&&gyQ4uWGZQN2{;yCx-rw(di&0=YUh1j%l;-~~kG>WLO=Cl7< zX65Gj@k}5Ny6HfVDHFgR=(M8tZ5qDln-z}fs~j)`?K1!Zz$;E zc=axMurET#7B#^$EmbNCDt6Qq7b!8IgCm3I+P;$|tvNCAV-0kFoQ1^)JkOMYwDAC0lq3g0VwFFm1c z=Kw>)uju^sq8P(r=?Uf>Z3hk|eiVtNA~rZdbD0=|3?^>$AQU{mw9+JS4@MbV1Pmfk zp}cZktp8UQYgoK`*OELS0RSzhT^{bNHGFtjeIepcb#9i)s|wJn%+|5&>PG>wM( zXI0*}2*NV;*OD7pJiK~`Le=9ZSU^@}6+rOJcpO&5F{8?G*8cHFasBq( z+GmyaHv@@#jwc^fwUarqH_RwyRgFUPZmZQJ&@|^(HH%$sDSH&nx4hg;NNzL;+!swB z^x`u~RdMuOK=TKD;;(V8JrsLU=g$r6o(N!K2{DUYh#DJSd#n(rr_LWcIS|Uk_$<;u z#A?!&M?FVgkCpwM+S?t=Hwy4VG1$E09sr=t?UZQ?z;ubN$83oKfU6@wzARPYL8MfM z*}S8c7yx)X93OY2f8)pfR>(E2AbQNgyhfbr_IX)AZj=IpyDLbo!&P_yxuG0`t0O?Y zB-fx}SF#k?dzSJZcbrK39Cp0$H+#NGyuQi^w_}`~rYnWw_k$P-Kt^P<5X{;-|Ml%o z+*t8kL&L^TkDl^o8o?qV7G^P%aQryMfCLDN03n29sRiB4ECs%M5CFsh1S}R8CdGSX zPPIvtZSYi}c;gbqDQ`x8cZY}sxJV$c$}&J;+@xeK6A@zyK{pdif$vg?1_0nziDDiS zVd5w$U=+#588=~htYGBi$qpxjW*60J*a=TK^SBLjj8sDPFou_w=?8sYZFa55+u0tc zq72ll^0kyTd_&MB2T5*&+EhpnS-{Y!#9clw1PB};6Hn^Pf{ zHh0H1w;yPUKZK{~n*4ijhv1 zDZQ&9I#!|dJ%xzhp%5l68HfdUDwu%)CYA(ek_$D(wXN$5mZ}g41xNbNwDnHRJWcve zp)}Iw8C?o|#|s^c1I*>Ab2?qss|B@E zju`a!9-J?BGrkl!P2$CW(Q!|M7aMqjdGG}D;0flz6MPpEU9#n=t5ph~);l6tS^V4& zpL=C}7T;Nm5wp&NvuYk*DGD*)!8HE1QeZ&HmM0zl0lc8xmy05{i-m22IGe7Ln8zGE z7|C8=T{QC5pC;)A54KcYlU;3D{>}&G@0^yF)Y*y>MTAJWVyWcwKdjx|u9lbEi=+TZ zXhV&cJzxCB?@K=Sk-1`?(>W827gTRkT{$%9`wrYUT`4dp9=fP?aS(ASogAkYr>P|7 zF?)!Xt!yc0+mBg;fM^%qtkk8z03al}#7ik)S%5T^#5`svSYa$KF&Zj%U01ape_eI# zjgyXr@sQM|zzCD-$`qBvJZ2|2c;)CnUzzUL$-1(TvtI@} z_qICn(usoMl$k(S>V%1^LdH5GOjHt&@T}fR7X2L|Whrn7rmk5NmEL=YU_^{V@a(})j!g2b z-kTvBQKGab!n1p?dN3+h$_(qD`%w!38}beHpE*2RZ4!?R#GKZm@!Yk~Y$^5}J9OAN zlSA`s(Tg>Urkq#Kc+niKl<%*5h+t*$i$C~YqY(oDJhynGhPF?J`Tn|x#^At}L+5p^ z4XqEUkSH;A>Fh~9oxW$s;5mXHgBR-<%!4PG2Tw2$o?sq4!8~|^dCX}ZoH;B6&0Hv-m)?ar~Q}Ce$jU2-Z(#) z_u%BaomaQ1Np@xbp34*2YhQf$ff8Hi@B(f)I}hgvr`xd^#qxLNtz0KYi15H(*Ys9( zx9lt}Qf>Y**83~BJL%|1xnXM+@!4LATciXdy4nKn{HIs!9DwuEdmk-uSWZ^`j-ZC} z(Px&y2wX23E_kP326>Jj)Y+Ve3Pz@u?D@b@w0U^LSkZyunIm(m9h(6F(czkI=aG*% zeQRFz4w?1c5O@2|(t8*ZWO6CElUrA+3Q=Y%^TT@jBuk>!psccd)6aH2_iwwNd~y8} zd?D?cm-B;@J2tEExc9i@L~=jwTh0&0(ZtTuzUxN?lUk_?WeG|sR|(|xd5`}~wgDC3 zIss+Q{A$%1JQx5~=Mg90-ZK%+Ui18;waw$R=)rf>ta&*W4(l6 zr4l7g+_(kJUxAUTI2p)z2cCi8P#_TTjgCxE05Can#(UpJtr(mOq!`ZE`N6mE*qrJk zm6SV612q-HOwxQ&xW5alESF$t)H)T1DMZ2Y?vqWM?>Kha4n1qdIX^u%S_jyGSn;C(JBnQeb9^7!OhTat&})bWvx~ z%y8$Kye-v#|2GXHIyiK(t9>E$;Q2T|n6m|k#&fEFp-%4D93a!x9NYX|$FbV!qk{cC z`$7}|LUD)=^IBM-Vy-Bb7xhz%VCu~4!S^`$rAnS)9z4N3G8X?2Wo$xR9!@PD00000 LNkvXXu0mjfLid5T diff --git a/app/assets/images/solarized-light-scheme-preview.png b/app/assets/images/solarized-light-scheme-preview.png index 7da5d2d20904fc3359d47e00164b97ffd77d6621..c50db75449b974294a612f3215a673c7c2217f80 100644 GIT binary patch delta 3091 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 literal 4746 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 Y142@13Yb8Fj{pDw07*qoM6N<$f diff --git a/app/assets/images/switch_icon.png b/app/assets/images/switch_icon.png index 6b8bde41bc95c15df2f7ce6d18330cce1181a76c..c6b6c8d9521f64b00990ca5352c8ce269e9a3e4a 100644 GIT binary patch delta 214 zcmV;{04e{i3FiTj8Gi%-008(hmf-*Z0Io?yK~#7FV;CS{>txFSgFu7{Z8kaBIWRyV z1B_wYX|omvtv|T`cgVC6vSF}c03s;7?(*Lq4t@UnU2=AJW^iTzA_%k>^%nONgThOG zEBzS#7(ft+Frj_0Jp&9fAegox*c=R{{1|PvTHke)u$O{BJ5Lci1_%Tbw%*patbk7Q z{%-??wpUS1un}@h^ZMua#%~@RR`@ZXInvq6ZwWRBBQYIZ92hVm3Wqid0J3*sEW}74 QDgXcg07*qoM6N<$g5^(Cz5oCK literal 1197 zcmaJ=PiPcp6d#E#MVew0wb6rqoy1bX-PzgAj_$a#o86hsW|N&|ySw1hlarY*$q;A0 zapp^Qlax|SL;^~BXuufoq=;fY2;zT)wugomii${uf)qR{q(=`$eY45N9=bke-oJV8 z_ulutH}l0pwtdU4Eeyl7=LVD#$gS{peAEUUKfhW9v6E)1v`j{5O>;4mGRQDSIZGSG zC9D}!$5wEhVcL4kN|jdCyr>f^sQEF$y5&GN!^D$yN7KhKMZoJ2(CQ!UC0)U07hU(T)+Y{>&+rSk;_uU+e8X4}OY7!gyh}tljpb`t6 zt{uU0HehXU{hHvyxVq+Wd@$r6sTruM?+>-CjcAXS@VkEh7uc&zIhZS9k4(5a3~r>` zFXf1`i#1Bz3L!_Ew^$q_lz3yrL2_9@U#gmJ+P-67BSTfiob6G~)^SdeSilIHrXe0o zDRL&0iS~wNh37NjbUMPPQ_-j(A3PxNe5{$P5PibJHf`n_?{bBfx&AU(4pdgKYffUL z-z64m99lG6_Y!G|x1MXX?xnY7E(gtU{%HR*>ZS|!$5%HN7mSV7$2RP{3yZzkHot(M z;@e`rk^#B2v{Wb*;_-MQkw_+!pbrfVrBW%-Wm)d)>yso2IOw2&6F7VY4@80ve2oBM zjlcvy=!Y5vCdk3TK~WUJsVGV|n}rhje12wp_eJQShn5OuhS}7yZTpVTx^{_)w2~bv zS4PbG{JHa&e!O=5;o~Py|9bh)>(lIuC5VT)rz%~Gi+^N4`7ZRa^3|i6)q9;c-+Yjp zOU-TnW@Y(C*9y9OoMXN4ox%bMw$xpYb7argGmAqPb@a(6~UoJms zKYQxgMC8vMU(ccwkG{Y5_sq|~ziMYr&fZ_$^V`247I(`(EJHyimnkS$(?^cK1rl3* ArvLx| diff --git a/app/assets/images/trans_bg.gif b/app/assets/images/trans_bg.gif index 5f6ed04a43c97debddc5c6be5697ba74d0504adf..1a1c9c15ec71a58db869578399068cf313c51599 100644 GIT binary patch delta 26 hcmXpqoFF6Wz$DNjyRy1gTBgNsliB;r9nDM()&Obx2($nI delta 27 jcmXpsnjj;_z$DlrJF{Ji=T5TFDX+O#BXiG-FjxZsY}g3f diff --git a/app/assets/images/white-scheme-preview.png b/app/assets/images/white-scheme-preview.png index d32b7485e1ea9cbb451595f8ec2a10f88aa9f69b..fc4c40b9227cced4692d9c3f0b3e09d62894fe3e 100644 GIT binary patch literal 3751 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 literal 5617 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 Date: Sun, 18 Jan 2015 10:29:37 -0500 Subject: [PATCH 004/124] Fix various typos signe-in -> signed-in go_to_gihub_for_permissions -> go_to_github_for_permissions descendand -> descendant behavour -> behaviour recepient_email -> recipient_email generate_fingerpint -> generate_fingerprint dependes -> depends Cant't -> Can't wisit -> visit notifcation -> notification sufficent_scope -> sufficient_scope? levet -> level --- app/controllers/application_controller.rb | 2 +- app/controllers/github_imports_controller.rb | 6 +++--- app/helpers/tree_helper.rb | 2 +- app/mailers/emails/merge_requests.rb | 2 +- app/mailers/notify.rb | 4 ++-- app/models/key.rb | 4 ++-- app/models/merge_request.rb | 4 ++-- app/models/network/graph.rb | 2 +- app/models/note.rb | 2 +- app/services/notification_service.rb | 2 +- app/services/oauth2/access_token_validation_service.rb | 4 ++-- app/services/projects/create_service.rb | 2 +- 12 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6da4f91c3f..ad13a0ac3e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -49,7 +49,7 @@ class ApplicationController < ActionController::Base end def authenticate_user!(*args) - # If user is not signe-in and tries to access root_path - redirect him to landing page + # If user is not signed-in and tries to access root_path - redirect him to landing page if current_application_settings.home_page_url.present? if current_user.nil? && controller_name == 'dashboard' && action_name == 'show' redirect_to current_application_settings.home_page_url and return diff --git a/app/controllers/github_imports_controller.rb b/app/controllers/github_imports_controller.rb index c96bef598b..86e20b1664 100644 --- a/app/controllers/github_imports_controller.rb +++ b/app/controllers/github_imports_controller.rb @@ -58,11 +58,11 @@ class GithubImportsController < ApplicationController def github_auth if current_user.github_access_token.blank? - go_to_gihub_for_permissions + go_to_github_for_permissions end end - def go_to_gihub_for_permissions + def go_to_github_for_permissions redirect_to client.auth_code.authorize_url({ redirect_uri: callback_github_import_url, scope: "repo, user, user:email" @@ -70,6 +70,6 @@ class GithubImportsController < ApplicationController end def github_unauthorized - go_to_gihub_for_permissions + go_to_github_for_permissions end end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index d316213b1f..b614fb67ac 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -113,7 +113,7 @@ module TreeHelper tree_join(@ref, file) end - # returns the relative path of the first subdir that doesn't have only one directory descendand + # returns the relative path of the first subdir that doesn't have only one directory descendant def flatten_tree(tree) subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path) if subtree.count == 1 && subtree.first.dir? diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 9ecdac87d7..7f6c855c30 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -56,7 +56,7 @@ module Emails end end - # Over rides default behavour to show source/target + # Over rides default behaviour to show source/target # Formats arguments into a String suitable for use as an email subject # # extra - Extra Strings to be inserted into the subject diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 6d671e6e0b..5ae07d771f 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -26,8 +26,8 @@ class Notify < ActionMailer::Base delay_for(2.seconds) end - def test_email(recepient_email, subject, body) - mail(to: recepient_email, + def test_email(recipient_email, subject, body) + mail(to: recipient_email, subject: subject, body: body.html_safe, content_type: 'text/html' diff --git a/app/models/key.rb b/app/models/key.rb index 65a426d1f8..d2d1af6882 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -19,7 +19,7 @@ class Key < ActiveRecord::Base belongs_to :user - before_validation :strip_white_space, :generate_fingerpint + before_validation :strip_white_space, :generate_fingerprint validates :title, presence: true, length: { within: 0..255 } validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }, uniqueness: true @@ -76,7 +76,7 @@ class Key < ActiveRecord::Base private - def generate_fingerpint + def generate_fingerprint self.fingerprint = nil return unless key.present? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index de0ee0e2c5..9bc0afa603 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -330,7 +330,7 @@ class MergeRequest < ActiveRecord::Base end # Return array of possible target branches - # dependes on target project of MR + # depends on target project of MR def target_branches if target_project.nil? [] @@ -340,7 +340,7 @@ class MergeRequest < ActiveRecord::Base end # Return array of possible source branches - # dependes on source project of MR + # depends on source project of MR def source_branches if source_project.nil? [] diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index 43979b5e80..7f761f2bdf 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -84,7 +84,7 @@ module Network skip += self.class.max_count end else - # Cant't find the target commit in the repo. + # Can't find the target commit in the repo. offset = 0 end end diff --git a/app/models/note.rb b/app/models/note.rb index e99bc2668d..b78c8b343a 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -480,7 +480,7 @@ class Note < ActiveRecord::Base end # FIXME: Hack for polymorphic associations with STI - # For more information wisit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations + # For more information visit http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations def noteable_type=(sType) super(sType.to_s.classify.constantize.base_class.to_s) end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 5a89c5d293..72c9149378 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -242,7 +242,7 @@ class NotificationService users end - # Build a list of users based on group notifcation settings + # Build a list of users based on group notification settings def select_users_group_setting(project, project_members, global_setting, users_global_level_watch) uids = users_group_notification(project, Notification::N_WATCH) diff --git a/app/services/oauth2/access_token_validation_service.rb b/app/services/oauth2/access_token_validation_service.rb index 9528348975..5a3b94129f 100644 --- a/app/services/oauth2/access_token_validation_service.rb +++ b/app/services/oauth2/access_token_validation_service.rb @@ -13,7 +13,7 @@ module Oauth2::AccessTokenValidationService elsif token.revoked? return REVOKED - elsif !self.sufficent_scope?(token, scopes) + elsif !self.sufficient_scope?(token, scopes) return INSUFFICIENT_SCOPE else @@ -24,7 +24,7 @@ module Oauth2::AccessTokenValidationService protected # True if the token's scope is a superset of required scopes, # or the required scopes is empty. - def sufficent_scope?(token, scopes) + def sufficient_scope?(token, scopes) if scopes.blank? # if no any scopes required, the scopes of token is sufficient. return true diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 31226b7504..139de70114 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -7,7 +7,7 @@ module Projects def execute @project = Project.new(params) - # Reset visibility levet if is not allowed to set it + # Reset visibility level if is not allowed to set it unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level]) @project.visibility_level = default_features.visibility_level end From 39e54e21cb5dfaddaf4c83bc4509c951675091ea Mon Sep 17 00:00:00 2001 From: jubianchi Date: Sun, 18 Jan 2015 22:17:10 +0100 Subject: [PATCH 005/124] Handle errors on API when a project does not have a repository (Closes #6289) --- lib/api/repositories.rb | 37 ++++++++++++++++++++------ spec/requests/api/repositories_spec.rb | 16 +++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 03a556a2c5..b259914a01 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -58,11 +58,13 @@ module API # ref_name (optional) - The name of a repository branch or tag, if not given the default branch is used # Example Request: # GET /projects/:id/repository/tree - get ":id/repository/tree" do + get ':id/repository/tree' do ref = params[:ref_name] || user_project.try(:default_branch) || 'master' path = params[:path] || nil commit = user_project.repository.commit(ref) + not_found!('Tree') unless commit + tree = user_project.repository.tree(commit.id, path) present tree.sorted_entries, with: Entities::RepoTreeObject @@ -100,14 +102,18 @@ module API # sha (required) - The blob's sha # Example Request: # GET /projects/:id/repository/raw_blobs/:sha - get ":id/repository/raw_blobs/:sha" do + get ':id/repository/raw_blobs/:sha' do ref = params[:sha] repo = user_project.repository - blob = Gitlab::Git::Blob.raw(repo, ref) + begin + blob = Gitlab::Git::Blob.raw(repo, ref) + rescue + not_found! 'Blob' + end - not_found! "Blob" unless blob + not_found! 'Blob' unless blob env['api.format'] = :txt @@ -122,13 +128,23 @@ module API # sha (optional) - the commit sha to download defaults to the tip of the default branch # Example Request: # GET /projects/:id/repository/archive - get ":id/repository/archive", requirements: { format: Gitlab::Regex.archive_formats_regex } do + get ':id/repository/archive', + requirements: { format: Gitlab::Regex.archive_formats_regex } do authorize! :download_code, user_project - file_path = ArchiveRepositoryService.new.execute(user_project, params[:sha], params[:format]) + + begin + file_path = ArchiveRepositoryService.new.execute( + user_project, + params[:sha], + params[:format]) + rescue + not_found!('File') + end if file_path && File.exists?(file_path) data = File.open(file_path, 'rb').read - header["Content-Disposition"] = "attachment; filename=\"#{File.basename(file_path)}\"" + basename = File.basename(file_path) + header['Content-Disposition'] = "attachment; filename=\"#{basename}\"" content_type MIME::Types.type_for(file_path).first.content_type env['api.format'] = :binary present data @@ -161,7 +177,12 @@ module API get ':id/repository/contributors' do authorize! :download_code, user_project - present user_project.repository.contributors, with: Entities::Contributor + begin + present user_project.repository.contributors, + with: Entities::Contributor + rescue + not_found! + end end end end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index beae71c02d..5518d2df56 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -101,6 +101,14 @@ describe API::API, api: true do json_response.first['type'].should == 'tree' json_response.first['mode'].should == '040000' end + + it 'should return a 404 for unknown ref' do + get api("/projects/#{project.id}/repository/tree?ref_name=foo", user) + response.status.should == 404 + + json_response.should be_an Object + json_response['message'] == '404 Tree Not Found' + end end context "unauthorized user" do @@ -145,6 +153,14 @@ describe API::API, api: true do get api("/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}", user) response.status.should == 200 end + + it 'should return a 404 for unknown blob' do + get api("/projects/#{project.id}/repository/raw_blobs/123456", user) + response.status.should == 404 + + json_response.should be_an Object + json_response['message'] == '404 Blob Not Found' + end end describe "GET /projects/:id/repository/archive(.:format)?:sha" do From 505a492cd87be7683827c5f46a05b6a7dddffc86 Mon Sep 17 00:00:00 2001 From: Michael Clarke Date: Wed, 5 Nov 2014 20:45:18 +0000 Subject: [PATCH 006/124] Only count the user's last vote --- CHANGELOG | 2 +- .../stylesheets/generic/typography.scss | 4 + app/models/concerns/issuable.rb | 16 ++- app/models/note.rb | 17 +++ app/views/projects/notes/_note.html.haml | 26 +++-- spec/lib/votes_spec.rb | 100 +++++++++++++----- 6 files changed, 128 insertions(+), 37 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6784c1f258..7962d9aa35 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -14,7 +14,7 @@ v 7.8.0 - - - - - + - Only count a user's vote once on a merge request or issue (Michael Clarke) - - - diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss index 385a627b4b..58243bc5ba 100644 --- a/app/assets/stylesheets/generic/typography.scss +++ b/app/assets/stylesheets/generic/typography.scss @@ -128,3 +128,7 @@ a:focus { textarea.js-gfm-input { font-family: $monospace_font; } + +.strikethrough { + text-decoration: line-through; +} \ No newline at end of file diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index f49708fd6e..b8bee0d0ec 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -88,7 +88,7 @@ module Issuable # Return the number of -1 comments (downvotes) def downvotes - notes.select(&:downvote?).size + filter_superceded_votes(notes.select(&:downvote?), notes).size end def downvotes_in_percent @@ -101,7 +101,7 @@ module Issuable # Return the number of +1 comments (upvotes) def upvotes - notes.select(&:upvote?).size + filter_superceded_votes(notes.select(&:upvote?), notes).size end def upvotes_in_percent @@ -154,4 +154,16 @@ module Issuable self.labels << label end end + + private + + def filter_superceded_votes(votes, notes) + filteredvotes = [] + votes + votes.each do |vote| + if vote.superceded?(notes) + filteredvotes.delete(vote) + end + end + filteredvotes + end end diff --git a/app/models/note.rb b/app/models/note.rb index e99bc2668d..1b7e412e9c 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -459,6 +459,23 @@ class Note < ActiveRecord::Base ) end + def superceded?(notes) + return false unless vote? + notes.each do |note| + next if note == self + if note.vote? && + self[:author_id] == note[:author_id] && + self[:created_at] <= note[:created_at] + return true + end + end + false + end + + def vote? + upvote? || downvote? + end + def votable? for_issue? || (for_merge_request? && !for_diff_line?) end diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 691c169b62..88c7b7ccf1 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -28,14 +28,24 @@ %span.note-last-update = note_timestamp(note) - - if note.upvote? - %span.vote.upvote.label.label-success - %i.fa.fa-thumbs-up - \+1 - - if note.downvote? - %span.vote.downvote.label.label-danger - %i.fa.fa-thumbs-down - \-1 + - if note.superceded?(@notes) + - if note.upvote? + %span.vote.upvote.label.label-gray.strikethrough + %i.fa.fa-thumbs-up + \+1 + - if note.downvote? + %span.vote.downvote.label.label-gray.strikethrough + %i.fa.fa-thumbs-down + \-1 + - else + - if note.upvote? + %span.vote.upvote.label.label-success + %i.fa.fa-thumbs-up + \+1 + - if note.downvote? + %span.vote.downvote.label.label-danger + %i.fa.fa-thumbs-down + \-1 .note-body diff --git a/spec/lib/votes_spec.rb b/spec/lib/votes_spec.rb index a3c353d5ea..63692814b9 100644 --- a/spec/lib/votes_spec.rb +++ b/spec/lib/votes_spec.rb @@ -20,11 +20,17 @@ describe Issue, 'Votes' do issue.upvotes.should == 1 end - it "should recognize multiple +1 notes" do - add_note "+1 This is awesome" - add_note "+1 I want this" + it 'should recognize multiple +1 notes' do + add_note '+1 This is awesome', create(:user) + add_note '+1 I want this', create(:user) issue.upvotes.should == 2 end + + it 'should not count 2 +1 votes from the same user' do + add_note '+1 This is awesome' + add_note '+1 I want this' + issue.upvotes.should == 1 + end end describe "#downvotes" do @@ -45,8 +51,8 @@ describe Issue, 'Votes' do end it "should recognize multiple -1 notes" do - add_note "-1 This is bad" - add_note "-1 Away with this" + add_note('-1 This is bad', create(:user)) + add_note('-1 Away with this', create(:user)) issue.downvotes.should == 2 end end @@ -73,11 +79,17 @@ describe Issue, 'Votes' do end it "should recognize multiple notes" do - add_note "+1 This is awesome" - add_note "-1 This is bad" - add_note "+1 I want this" + add_note('+1 This is awesome', create(:user)) + add_note('-1 This is bad', create(:user)) + add_note('+1 I want this', create(:user)) issue.votes_count.should == 3 end + + it 'should not count 2 -1 votes from the same user' do + add_note '-1 This is suspicious' + add_note '-1 This is bad' + issue.votes_count.should == 1 + end end describe "#upvotes_in_percent" do @@ -90,17 +102,17 @@ describe Issue, 'Votes' do issue.upvotes_in_percent.should == 100 end - it "should count multiple +1 notes as 100%" do - add_note "+1 This is awesome" - add_note "+1 I want this" + it 'should count multiple +1 notes as 100%' do + add_note('+1 This is awesome', create(:user)) + add_note('+1 I want this', create(:user)) issue.upvotes_in_percent.should == 100 end - it "should count fractions for multiple +1 and -1 notes correctly" do - add_note "+1 This is awesome" - add_note "+1 I want this" - add_note "-1 This is bad" - add_note "+1 me too" + it 'should count fractions for multiple +1 and -1 notes correctly' do + add_note('+1 This is awesome', create(:user)) + add_note('+1 I want this', create(:user)) + add_note('-1 This is bad', create(:user)) + add_note('+1 me too', create(:user)) issue.upvotes_in_percent.should == 75 end end @@ -115,22 +127,58 @@ describe Issue, 'Votes' do issue.downvotes_in_percent.should == 100 end - it "should count multiple -1 notes as 100%" do - add_note "-1 This is bad" - add_note "-1 Away with this" + it 'should count multiple -1 notes as 100%' do + add_note('-1 This is bad', create(:user)) + add_note('-1 Away with this', create(:user)) issue.downvotes_in_percent.should == 100 end - it "should count fractions for multiple +1 and -1 notes correctly" do - add_note "+1 This is awesome" - add_note "+1 I want this" - add_note "-1 This is bad" - add_note "+1 me too" + it 'should count fractions for multiple +1 and -1 notes correctly' do + add_note('+1 This is awesome', create(:user)) + add_note('+1 I want this', create(:user)) + add_note('-1 This is bad', create(:user)) + add_note('+1 me too', create(:user)) issue.downvotes_in_percent.should == 25 end end - def add_note(text) - issue.notes << create(:note, note: text, project: issue.project) + describe '#filter_superceded_votes' do + + it 'should count a users vote only once amongst multiple votes' do + add_note('-1 This needs work before I will accept it') + add_note('+1 I want this', create(:user)) + add_note('+1 This is is awesome', create(:user)) + add_note('+1 this looks good now') + add_note('+1 This is awesome', create(:user)) + add_note('+1 me too', create(:user)) + issue.downvotes.should == 0 + issue.upvotes.should == 5 + end + + it 'should count each users vote only once' do + add_note '-1 This needs work before it will be accepted' + add_note '+1 I like this' + add_note '+1 I still like this' + add_note '+1 I really like this' + add_note '+1 Give me this now!!!!' + p issue.downvotes.should == 0 + p issue.upvotes.should == 1 + end + + it 'should count a users vote only once without caring about comments' do + add_note '-1 This needs work before it will be accepted' + add_note 'Comment 1' + add_note 'Another comment' + add_note '+1 vote' + add_note 'final comment' + p issue.downvotes.should == 0 + p issue.upvotes.should == 1 + end + + end + + def add_note(text, author = issue.author) + issue.notes << create(:note, note: text, project: issue.project, + author_id: author.id) end end From 46755a7bfec1a35ff7967cf92ed8da3d911d32f0 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 20 Jan 2015 09:16:26 +0200 Subject: [PATCH 007/124] Disable 'check all issues' checkbox for unprivileged users. --- app/views/projects/issues/_issues.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml index 010ca3b68b..816851a8ab 100644 --- a/app/views/projects/issues/_issues.html.haml +++ b/app/views/projects/issues/_issues.html.haml @@ -1,6 +1,6 @@ .append-bottom-10 .check-all-holder - = check_box_tag "check_all_issues", nil, false, class: "check_all_issues left" + = check_box_tag "check_all_issues", nil, false, class: "check_all_issues left", disabled: !can?(current_user, :modify_issue, @project) = render 'shared/issuable_filter' .clearfix From 47e061466968dac72ba488eee0d3c44f568b11ab Mon Sep 17 00:00:00 2001 From: GitLab Date: Tue, 20 Jan 2015 00:22:40 +0100 Subject: [PATCH 008/124] Disable turbolink on links pointing out to ci services --- app/views/projects/merge_requests/show/_mr_ci.html.haml | 6 +++--- app/views/projects/show.html.haml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/projects/merge_requests/show/_mr_ci.html.haml b/app/views/projects/merge_requests/show/_mr_ci.html.haml index 941b15d3b3..ee7fd0ef15 100644 --- a/app/views/projects/merge_requests/show/_mr_ci.html.haml +++ b/app/views/projects/merge_requests/show/_mr_ci.html.haml @@ -3,21 +3,21 @@ %i.fa.fa-check %span CI build passed for #{@merge_request.last_commit_short_sha}. - = link_to "Build page", ci_build_details_path(@merge_request) + = link_to "Build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" .ci_widget.ci-failed{style: "display:none"} %i.fa.fa-times %span CI build failed for #{@merge_request.last_commit_short_sha}. - = link_to "Build page", ci_build_details_path(@merge_request) + = link_to "Build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" - [:running, :pending].each do |status| .ci_widget{class: "ci-#{status}", style: "display:none"} %i.fa.fa-clock-o %span CI build #{status} for #{@merge_request.last_commit_short_sha}. - = link_to "Build page", ci_build_details_path(@merge_request) + = link_to "Build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" .ci_widget %i.fa.fa-spinner diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index af6e4567c1..737a34decd 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -68,11 +68,11 @@ - @project.ci_services.each do |ci_service| - if ci_service.active? && ci_service.respond_to?(:builds_path) - if ci_service.respond_to?(:status_img_path) - = link_to ci_service.builds_path do + = link_to ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' do = image_tag ci_service.status_img_path, alt: "build status" - else %span.light CI provided by - = link_to ci_service.title, ci_service.builds_path + = link_to ci_service.title, ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' - if readme .tab-pane#tab-readme From 9371c6b90136547e3622622510af9894fc27aeb0 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 20 Jan 2015 16:46:27 -0800 Subject: [PATCH 009/124] Add issue tracker services. --- .../project_services/issue_tracker_service.rb | 14 +++++ app/models/project_services/jira_service.rb | 59 +++++++++++++++++++ .../project_services/redmine_service.rb | 51 ++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 app/models/project_services/issue_tracker_service.rb create mode 100644 app/models/project_services/jira_service.rb create mode 100644 app/models/project_services/redmine_service.rb diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb new file mode 100644 index 0000000000..4ba2f5a9ca --- /dev/null +++ b/app/models/project_services/issue_tracker_service.rb @@ -0,0 +1,14 @@ +class IssueTrackerService < Service + + def project_url + # implement inside child + end + + def issues_url + # implement inside child + end + + def new_issue_url + # implement inside child + end +end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb new file mode 100644 index 0000000000..f83f01c55b --- /dev/null +++ b/app/models/project_services/jira_service.rb @@ -0,0 +1,59 @@ +class JiraService < IssueTrackerService + + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + + def title + if self.properties && self.properties['title'].present? + self.properties['title'] + else + 'JIRA' + end + end + + def description + if self.properties && self.properties['description'].present? + self.properties['description'] + else + 'Jira issue tracker' + end + end + + def to_param + 'jira' + end + + def fields + [ + { type: 'text', name: 'title', placeholder: title }, + { type: 'text', name: 'description', placeholder: description }, + { type: 'text', name: 'project_url', placeholder: 'Project url' }, + { type: 'text', name: 'issues_url', placeholder: 'Issue url'}, + { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url'} + ] + end + + def initialize_properties + if properties.nil? + if enabled_in_gitlab_config + self.properties = { + title: issues_tracker['title'], + project_url: issues_tracker['project_url'], + issues_url: issues_tracker['issues_url'], + new_issue_url: issues_tracker['new_issue_url'] + } + end + end + end + + private + + def enabled_in_gitlab_config + Gitlab.config.issues_tracker && + Gitlab.config.issues_tracker.values.any? && + issues_tracker + end + + def issues_tracker + Gitlab.config.issues_tracker['jira'] + end +end diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb new file mode 100644 index 0000000000..8052fb2246 --- /dev/null +++ b/app/models/project_services/redmine_service.rb @@ -0,0 +1,51 @@ +class RedmineService < IssueTrackerService + + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + + def title + 'Redmine' + end + + def description + 'Redmine issue tracker' + end + + def to_param + 'redmine' + end + + def fields + [ + { type: 'text', name: 'title', placeholder: title }, + { type: 'text', name: 'description', placeholder: description }, + { type: 'text', name: 'project_url', placeholder: 'Project url' }, + { type: 'text', name: 'issues_url', placeholder: 'Issue url'}, + { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url'} + ] + end + + def initialize_properties + if properties.nil? + if enabled_in_gitlab_config + self.properties = { + title: issues_tracker['title'], + project_url: issues_tracker['project_url'], + issues_url: issues_tracker['issues_url'], + new_issue_url: issues_tracker['new_issue_url'] + } + end + end + end + + private + + def enabled_in_gitlab_config + Gitlab.config.issues_tracker && + Gitlab.config.issues_tracker.values.any? && + issues_tracker + end + + def issues_tracker + Gitlab.config.issues_tracker['redmine'] + end +end From 62c00661c43334f8e2bbed508d9517529dbee7e0 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 20 Jan 2015 16:47:29 -0800 Subject: [PATCH 010/124] Allow creation of the jira and redmine services. --- app/controllers/projects/services_controller.rb | 6 ++++-- app/models/project.rb | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index b2ce99aeb4..15f47ed9c9 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -17,7 +17,8 @@ class Projects::ServicesController < Projects::ApplicationController def update if @service.update_attributes(service_params) - redirect_to edit_project_service_path(@project, @service.to_param) + redirect_to edit_project_service_path(@project, @service.to_param), + notice: 'Successfully updated.' else render 'edit' end @@ -41,7 +42,8 @@ class Projects::ServicesController < Projects::ApplicationController :title, :token, :type, :active, :api_key, :subdomain, :room, :recipients, :project_url, :webhook, :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, - :build_key, :server, :teamcity_url, :build_type + :build_key, :server, :teamcity_url, :build_type, + :description, :issues_url, :new_issue_url ) end end diff --git a/app/models/project.rb b/app/models/project.rb index a22f852de6..a90081ce73 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -68,6 +68,9 @@ class Project < ActiveRecord::Base has_one :bamboo_service, dependent: :destroy has_one :teamcity_service, dependent: :destroy has_one :pushover_service, dependent: :destroy + has_one :jira_service, dependent: :destroy + has_one :redmine_service, dependent: :destroy + has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link # Merge Requests for target project should be removed with it @@ -321,7 +324,7 @@ class Project < ActiveRecord::Base def available_services_names %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla - emails_on_push gemnasium slack pushover buildbox bamboo teamcity) + emails_on_push gemnasium slack pushover buildbox bamboo teamcity jira redmine) end def gitlab_ci? From 09de0bfc37c54b84d5a49dbce8c11cfd2f3cfb80 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 20 Jan 2015 16:55:12 -0800 Subject: [PATCH 011/124] Redmine doesn't require title and description change --- app/models/project_services/redmine_service.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index 8052fb2246..55841b5005 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -16,8 +16,6 @@ class RedmineService < IssueTrackerService def fields [ - { type: 'text', name: 'title', placeholder: title }, - { type: 'text', name: 'description', placeholder: description }, { type: 'text', name: 'project_url', placeholder: 'Project url' }, { type: 'text', name: 'issues_url', placeholder: 'Issue url'}, { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url'} From e9d6d1e51afa9f46f19748977739f7d2c078b84f Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 20 Jan 2015 16:55:35 -0800 Subject: [PATCH 012/124] Custom issue tracker service. --- app/models/project.rb | 3 +- .../custom_issue_tracker_service.rb | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 app/models/project_services/custom_issue_tracker_service.rb diff --git a/app/models/project.rb b/app/models/project.rb index a90081ce73..e501ccb59f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -70,6 +70,7 @@ class Project < ActiveRecord::Base has_one :pushover_service, dependent: :destroy has_one :jira_service, dependent: :destroy has_one :redmine_service, dependent: :destroy + has_one :custom_issue_tracker_service, dependent: :destroy has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link @@ -324,7 +325,7 @@ class Project < ActiveRecord::Base def available_services_names %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla - emails_on_push gemnasium slack pushover buildbox bamboo teamcity jira redmine) + emails_on_push gemnasium slack pushover buildbox bamboo teamcity jira redmine custom_issue_tracker) end def gitlab_ci? diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb new file mode 100644 index 0000000000..69e1b204ba --- /dev/null +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -0,0 +1,38 @@ +class CustomIssueTrackerService < IssueTrackerService + + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + + def title + if self.properties && self.properties['title'].present? + self.properties['title'] + else + 'Custom Issue Tracker' + end + end + + def description + if self.properties && self.properties['description'].present? + self.properties['description'] + else + 'Custom issue tracker' + end + end + + def to_param + title.parameterize + end + + def fields + [ + { type: 'text', name: 'title', placeholder: title }, + { type: 'text', name: 'description', placeholder: description }, + { type: 'text', name: 'project_url', placeholder: 'Project url' }, + { type: 'text', name: 'issues_url', placeholder: 'Issue url'}, + { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url'} + ] + end + + def initialize_properties + self.properties = {} if properties.nil? + end +end From 0eed5cace39fccba57e590c4fb9bea1e1cd8387d Mon Sep 17 00:00:00 2001 From: Vinnie Okada Date: Mon, 19 Jan 2015 04:24:00 +0000 Subject: [PATCH 013/124] Upgrade Sidekiq to 3.3 --- CHANGELOG | 2 +- Gemfile | 2 +- Gemfile.lock | 20 +++++++++++--------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6784c1f258..4643af549f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,7 +19,7 @@ v 7.8.0 - - - - - + - Upgrade Sidekiq to version 3.3 - - - diff --git a/Gemfile b/Gemfile index cdbc2963d4..96a1097d6d 100644 --- a/Gemfile +++ b/Gemfile @@ -118,7 +118,7 @@ gem "acts-as-taggable-on" # Background jobs gem 'slim' gem 'sinatra', require: nil -gem 'sidekiq', '2.17.8' +gem 'sidekiq', '~> 3.3' # HTTP requests gem "httparty" diff --git a/Gemfile.lock b/Gemfile.lock index d9ba4e3c17..18fae9b700 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -62,8 +62,8 @@ GEM activemodel (>= 3.2.0) activesupport (>= 3.2.0) json (>= 1.7) - celluloid (0.15.2) - timers (~> 1.1.0) + celluloid (0.16.0) + timers (~> 4.0.0) charlock_holmes (0.6.9.4) cliver (0.3.2) code_analyzer (0.4.3) @@ -244,6 +244,7 @@ GEM hike (1.2.3) hipchat (1.4.0) httparty + hitimes (1.2.2) html-pipeline (1.11.0) activesupport (>= 2) nokogiri (~> 1.4) @@ -495,12 +496,12 @@ GEM sexp_processor (4.4.0) shoulda-matchers (2.1.0) activesupport (>= 3.0.0) - sidekiq (2.17.8) - celluloid (= 0.15.2) - connection_pool (~> 2.0) + sidekiq (3.3.0) + celluloid (>= 0.16.0) + connection_pool (>= 2.0.0) json - redis (~> 3.1) - redis-namespace (~> 1.3) + redis (>= 3.0.6) + redis-namespace (>= 1.3.1) simple_oauth (0.1.9) simplecov (0.9.0) docile (~> 1.1.0) @@ -555,7 +556,8 @@ GEM thor (0.19.1) thread_safe (0.3.4) tilt (1.4.1) - timers (1.1.0) + timers (4.0.1) + hitimes timfel-krb5-auth (0.8) tinder (1.9.3) eventmachine (~> 1.0) @@ -716,7 +718,7 @@ DEPENDENCIES semantic-ui-sass (~> 0.16.1.0) settingslogic shoulda-matchers (~> 2.1.0) - sidekiq (= 2.17.8) + sidekiq (~> 3.3) simplecov sinatra six From 201b2f1099c6c1a963455d54bacbe473c7e27f95 Mon Sep 17 00:00:00 2001 From: Loic Dachary Date: Wed, 21 Jan 2015 18:57:54 +0100 Subject: [PATCH 014/124] Add return value example to ssh key creation Signed-off-by: Loic Dachary --- doc/api/users.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/api/users.md b/doc/api/users.md index b30a31decc..dd158f124d 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -322,6 +322,15 @@ Parameters: - `title` (required) - new SSH Key's title - `key` (required) - new SSH key +```json +{ + "created_at": "2015-01-21T17:44:33.512Z", + "key": "ssh-dss AAAAB3NzaC1kc3MAAACBAMLrhYgI3atfrSD6KDas1b/3n6R/HP+bLaHHX6oh+L1vg31mdUqK0Ac/NjZoQunavoyzqdPYhFz9zzOezCrZKjuJDS3NRK9rspvjgM0xYR4d47oNZbdZbwkI4cTv/gcMlquRy0OvpfIvJtjtaJWMwTLtM5VhRusRuUlpH99UUVeXAAAAFQCVyX+92hBEjInEKL0v13c/egDCTQAAAIEAvFdWGq0ccOPbw4f/F8LpZqvWDydAcpXHV3thwb7WkFfppvm4SZte0zds1FJ+Hr8Xzzc5zMHe6J4Nlay/rP4ewmIW7iFKNBEYb/yWa+ceLrs+TfR672TaAgO6o7iSRofEq5YLdwgrwkMmIawa21FrZ2D9SPao/IwvENzk/xcHu7YAAACAQFXQH6HQnxOrw4dqf0NqeKy1tfIPxYYUZhPJfo9O0AmBW2S36pD2l14kS89fvz6Y1g8gN/FwFnRncMzlLY/hX70FSc/3hKBSbH6C6j8hwlgFKfizav21eS358JJz93leOakJZnGb8XlWvz1UJbwCsnR2VEY8Dz90uIk1l/UqHkA= loic@call", + "title": "ABC", + "id": 4 +} +``` + ## Add SSH key for user Create new key owned by specified user. Available only for admin From d9b946fb3edb3288b6ae39c8882b287639a75cbb Mon Sep 17 00:00:00 2001 From: Loic Dachary Date: Wed, 21 Jan 2015 19:08:15 +0100 Subject: [PATCH 015/124] Document ssh key creation error Add the error code returned in the headers as well as an example of the JSON informative message returned in the body. Signed-off-by: Loic Dachary --- doc/api/users.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/doc/api/users.md b/doc/api/users.md index dd158f124d..71fa62bdd6 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -331,6 +331,22 @@ Parameters: } ``` +Will return created key with status `201 Created` on success. If an +error occurs a `400 Bad Request` is returned with a message explaining the error: + +```json +{ + "message": { + "fingerprint": [ + "has already been taken" + ], + "key": [ + "has already been taken" + ] + } +} +``` + ## Add SSH key for user Create new key owned by specified user. Available only for admin From 855fe20165715e34deb1e7153d02c811003095e5 Mon Sep 17 00:00:00 2001 From: Drew Blessing Date: Wed, 21 Jan 2015 14:02:30 -0600 Subject: [PATCH 016/124] Fix spinner icon to match others --- .../merge_requests/show/_remove_source_branch.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/merge_requests/show/_remove_source_branch.html.haml b/app/views/projects/merge_requests/show/_remove_source_branch.html.haml index 4fe5935bcf..9bf6a9d081 100644 --- a/app/views/projects/merge_requests/show/_remove_source_branch.html.haml +++ b/app/views/projects/merge_requests/show/_remove_source_branch.html.haml @@ -12,6 +12,6 @@ Failed to remove source branch '#{@merge_request.source_branch}' .remove_source_branch_in_progress.hide - %i.fa.fa-refresh.fa-spin + %i.fa.fa-spinner.fa-spin   Removing source branch '#{@merge_request.source_branch}'. Please wait. Page will be automatically reloaded.   From 772e321120b38353ca9dba1466fd480a8f9c9784 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 22 Jan 2015 06:44:49 +0000 Subject: [PATCH 017/124] Specify sidekiq patch version in changelog --- CHANGELOG | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4643af549f..8a8ac6c6c0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,7 +19,7 @@ v 7.8.0 - - - - - Upgrade Sidekiq to version 3.3 + - Upgrade Sidekiq to version 3.3.0 - - - @@ -1106,4 +1106,4 @@ v 0.8.0 - stability - security fixes - increased test coverage - - email notification + - email notification \ No newline at end of file From 2733e27864810819704f26be623d1b39cb7366b6 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 22 Jan 2015 07:19:24 +0000 Subject: [PATCH 018/124] Update CHANGELOG --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 8a8ac6c6c0..e68c6d0f58 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -19,7 +19,7 @@ v 7.8.0 - - - - - Upgrade Sidekiq to version 3.3.0 + - Upgrade Sidekiq gem to version 3.3.0 - - - From 9d271538a8e9ddff892a084e5c8a881bf2fdb0b0 Mon Sep 17 00:00:00 2001 From: Justin Whear Date: Mon, 2 Jun 2014 16:10:38 -0700 Subject: [PATCH 019/124] Add per-milestone issues API call --- lib/api/milestones.rb | 15 +++++++++++++++ spec/requests/api/milestones_spec.rb | 14 ++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index 2ea49359df..ca01fa4a57 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -75,6 +75,21 @@ module API render_api_error!("Failed to update milestone #{milestone.errors.messages}", 400) end end + + # Get all issues for single project milestone + # + # Parameters: + # id (required) - The ID of a project + # milestone_id (required) - The ID of a project milestone + # Example Request: + # GET /projects/:id/milestones/:milestone_id/issues + get ":id/milestones/:milestone_id/issues" do + authorize! :read_milestone, user_project + + @milestone = user_project.milestones.find(params[:milestone_id]) + present paginate(@milestone.issues), with: Entities::Issue + end + end end end diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index f0619a1c80..73432cb22a 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -96,4 +96,18 @@ describe API::API, api: true do state_event: 'close' end end + + describe "GET /projects/:id/milestones/:milestone_id/issues" do + it "should return project issues for a particular milestone" do + get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) + response.status.should == 200 + json_response.should be_an Array + json_response.first['milestone']['title'].should == milestone.title + end + + it "should return a 401 error if user not authenticated" do + get api("/projects/#{project.id}/milestones/#{milestone.id}/issues") + response.status.should == 401 + end + end end From e03f1af00a513a15085a69374a84a2f2df4689d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Wed, 21 Jan 2015 23:59:30 +0100 Subject: [PATCH 020/124] Fix the test and add documentation for the "per-milestone issues API call" --- CHANGELOG | 2 +- doc/api/milestones.md | 13 ++++++++ lib/api/milestones.rb | 2 +- spec/requests/api/milestones_spec.rb | 45 +++++++++++++++------------- 4 files changed, 39 insertions(+), 23 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e3fc463621..81908b0447 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -48,7 +48,7 @@ v 7.8.0 - - - - - + - Add a new API function that retrieves all issues assigned to a single milestone (Justin Whear and Hannes Rosenögger) - - - diff --git a/doc/api/milestones.md b/doc/api/milestones.md index 2f52532750..d48b3bcce8 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -72,3 +72,16 @@ Parameters: - `description` (optional) - The description of a milestone - `due_date` (optional) - The due date of the milestone - `state_event` (optional) - The state event of the milestone (close|activate) + +## Get all issues assigned to a single milestone + +Gets all issues assigned to a single project milestone. + +``` +GET /projects/:id/milestones/:milestone_id/issues +``` + +Parameters: + +- `id` (required) - The ID of a project +- `milestone_id` (required) - The ID of a project milestone diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index ca01fa4a57..c5cd73943f 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -76,7 +76,7 @@ module API end end - # Get all issues for single project milestone + # Get all issues for a single project milestone # # Parameters: # id (required) - The ID of a project diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index 73432cb22a..647033309b 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -8,48 +8,48 @@ describe API::API, api: true do before { project.team << [user, :developer] } - describe "GET /projects/:id/milestones" do - it "should return project milestones" do + describe 'GET /projects/:id/milestones' do + it 'should return project milestones' do get api("/projects/#{project.id}/milestones", user) response.status.should == 200 json_response.should be_an Array json_response.first['title'].should == milestone.title end - it "should return a 401 error if user not authenticated" do + it 'should return a 401 error if user not authenticated' do get api("/projects/#{project.id}/milestones") response.status.should == 401 end end - describe "GET /projects/:id/milestones/:milestone_id" do - it "should return a project milestone by id" do + describe 'GET /projects/:id/milestones/:milestone_id' do + it 'should return a project milestone by id' do get api("/projects/#{project.id}/milestones/#{milestone.id}", user) response.status.should == 200 json_response['title'].should == milestone.title json_response['iid'].should == milestone.iid end - it "should return 401 error if user not authenticated" do + it 'should return 401 error if user not authenticated' do get api("/projects/#{project.id}/milestones/#{milestone.id}") response.status.should == 401 end - it "should return a 404 error if milestone id not found" do + it 'should return a 404 error if milestone id not found' do get api("/projects/#{project.id}/milestones/1234", user) response.status.should == 404 end end - describe "POST /projects/:id/milestones" do - it "should create a new project milestone" do + describe 'POST /projects/:id/milestones' do + it 'should create a new project milestone' do post api("/projects/#{project.id}/milestones", user), title: 'new milestone' response.status.should == 201 json_response['title'].should == 'new milestone' json_response['description'].should be_nil end - it "should create a new project milestone with description and due date" do + it 'should create a new project milestone with description and due date' do post api("/projects/#{project.id}/milestones", user), title: 'new milestone', description: 'release', due_date: '2013-03-02' response.status.should == 201 @@ -57,29 +57,29 @@ describe API::API, api: true do json_response['due_date'].should == '2013-03-02' end - it "should return a 400 error if title is missing" do + it 'should return a 400 error if title is missing' do post api("/projects/#{project.id}/milestones", user) response.status.should == 400 end end - describe "PUT /projects/:id/milestones/:milestone_id" do - it "should update a project milestone" do + describe 'PUT /projects/:id/milestones/:milestone_id' do + it 'should update a project milestone' do put api("/projects/#{project.id}/milestones/#{milestone.id}", user), title: 'updated title' response.status.should == 200 json_response['title'].should == 'updated title' end - it "should return a 404 error if milestone id not found" do + it 'should return a 404 error if milestone id not found' do put api("/projects/#{project.id}/milestones/1234", user), title: 'updated title' response.status.should == 404 end end - describe "PUT /projects/:id/milestones/:milestone_id to close milestone" do - it "should update a project milestone" do + describe 'PUT /projects/:id/milestones/:milestone_id to close milestone' do + it 'should update a project milestone' do put api("/projects/#{project.id}/milestones/#{milestone.id}", user), state_event: 'close' response.status.should == 200 @@ -88,8 +88,8 @@ describe API::API, api: true do end end - describe "PUT /projects/:id/milestones/:milestone_id to test observer on close" do - it "should create an activity event when an milestone is closed" do + describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do + it 'should create an activity event when an milestone is closed' do Event.should_receive(:create) put api("/projects/#{project.id}/milestones/#{milestone.id}", user), @@ -97,15 +97,18 @@ describe API::API, api: true do end end - describe "GET /projects/:id/milestones/:milestone_id/issues" do - it "should return project issues for a particular milestone" do + describe 'GET /projects/:id/milestones/:milestone_id/issues' do + before do + milestone.issues << create(:issue) + end + it 'should return project issues for a particular milestone' do get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) response.status.should == 200 json_response.should be_an Array json_response.first['milestone']['title'].should == milestone.title end - it "should return a 401 error if user not authenticated" do + it 'should return a 401 error if user not authenticated' do get api("/projects/#{project.id}/milestones/#{milestone.id}/issues") response.status.should == 401 end From f937e059493037f3e18896a81693de81cf6a69a1 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 22 Jan 2015 15:51:20 +0100 Subject: [PATCH 021/124] Stop git zombie creation during force push check --- CHANGELOG | 2 +- lib/gitlab/force_push_check.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e3fc463621..25b539dac0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,7 +20,7 @@ v 7.8.0 - - - - - + - Stop git zombie creation during force push check - - - diff --git a/lib/gitlab/force_push_check.rb b/lib/gitlab/force_push_check.rb index 6a52cdba60..6ba2c3ad00 100644 --- a/lib/gitlab/force_push_check.rb +++ b/lib/gitlab/force_push_check.rb @@ -4,7 +4,7 @@ module Gitlab return false if project.empty_repo? if oldrev != Gitlab::Git::BLANK_SHA && newrev != Gitlab::Git::BLANK_SHA - missed_refs = IO.popen(%W(git --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev})).read + missed_refs, _ = Gitlab::Popen.popen(%W(git --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev})) missed_refs.split("\n").size > 0 else false From a63187f28b18e2feea16681b313166a982254e4e Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 22 Jan 2015 15:53:16 +0100 Subject: [PATCH 022/124] Don't create zombies with IO.popen The previous recommend incantation would leave the process we read from hanging around, even though it had finished. That gives you a 'defunct'/'zombie' process. --- doc/development/shell_commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/shell_commands.md b/doc/development/shell_commands.md index 1e51ad73e3..42f17e1953 100644 --- a/doc/development/shell_commands.md +++ b/doc/development/shell_commands.md @@ -108,7 +108,7 @@ In other repositories, such as gitlab-shell you can also use `IO.popen`. ```ruby # Safe IO.popen example -logs = IO.popen(%W(git log), chdir: repo_dir).read +logs = IO.popen(%W(git log), chdir: repo_dir) { |p| p.read } ``` Note that unlike `Gitlab::Popen.popen`, `IO.popen` does not capture standard error. From 7dd5656a5b352dd5df5dabeeebdb21d7ffd9ef03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20M=C3=A4enp=C3=A4=C3=A4?= Date: Wed, 15 Oct 2014 09:57:35 +0300 Subject: [PATCH 023/124] Implement edit via API for projects --- CHANGELOG | 1 + doc/api/projects.md | 25 ++++++ lib/api/projects.rb | 43 ++++++++++ spec/requests/api/projects_spec.rb | 131 +++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index f1346885ab..8468c9a7ae 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -181,6 +181,7 @@ v 7.4.0 - Fail harder in the backup script - Changes to Slack service structure, only webhook url needed - Zen mode for wiki and milestones (Robert Schilling) + - API: Add support for editing an existing project (Mika Mäenpää) - Move Emoji parsing to html-pipeline-gitlab (Robert Schilling) - Font Awesome 4.2 integration (Sullivan Senechal) - Add Pushover service integration (Sullivan Senechal) diff --git a/doc/api/projects.md b/doc/api/projects.md index 027a8ec2e7..d7804689c2 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -287,6 +287,31 @@ Parameters: - `visibility_level` (optional) - `import_url` (optional) +### Edit project + +Updates an existing project + +``` +PUT /projects/:id +``` + +Parameters: + +- `id` (required) - The ID of a project +- `name` (optional) - project name +- `path` (optional) - repository name for project +- `description` (optional) - short project description +- `default_branch` (optional) +- `issues_enabled` (optional) +- `merge_requests_enabled` (optional) +- `wiki_enabled` (optional) +- `snippets_enabled` (optional) +- `public` (optional) - if `true` same as setting visibility_level = 20 +- `visibility_level` (optional) + +On success, method returns 200 with the updated project. If parameters are +invalid, 400 is returned. + ### Fork project Forks a project into the user namespace of the authenticated user. diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 5b0c31f189..d96288bb98 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -200,6 +200,49 @@ module API end end + # Update an existing project + # + # Parameters: + # id (required) - the id of a project + # name (optional) - name of a project + # path (optional) - path of a project + # description (optional) - short project description + # issues_enabled (optional) + # merge_requests_enabled (optional) + # wiki_enabled (optional) + # snippets_enabled (optional) + # public (optional) - if true same as setting visibility_level = 20 + # visibility_level (optional) - visibility level of a project + # Example Request + # PUT /projects/:id + put ':id' do + attrs = attributes_for_keys [:name, + :path, + :description, + :default_branch, + :issues_enabled, + :merge_requests_enabled, + :wiki_enabled, + :snippets_enabled, + :public, + :visibility_level] + attrs = map_public_to_visibility_level(attrs) + authorize_admin_project + authorize! :rename_project, user_project if attrs[:name].present? + if attrs[:visibility_level].present? + authorize! :change_visibility_level, user_project + end + + ::Projects::UpdateService.new(user_project, + current_user, attrs).execute + + if user_project.valid? + present user_project, with: Entities::Project + else + render_validation_error!(user_project) + end + end + # Remove project # # Parameters: diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 3098b0f77f..26d1a8d193 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- require 'spec_helper' describe API::API, api: true do @@ -12,6 +13,24 @@ describe API::API, api: true do let(:snippet) { create(:project_snippet, author: user, project: project, title: 'example') } let(:project_member) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } let(:project_member2) { create(:project_member, user: user3, project: project, access_level: ProjectMember::DEVELOPER) } + let(:user4) { create(:user) } + let(:project3) { create(:project, + name: 'second_project', + path: 'second_project', + creator_id: user.id, + namespace: user.namespace, + merge_requests_enabled: false, + issues_enabled: false, wiki_enabled: false, + snippets_enabled: false, visibility_level: 0) } + let(:project_member3) { create(:project_member, + user: user4, + project: project3, + access_level: ProjectMember::MASTER) } + let(:project4) { create(:project, + name: 'third_project', + path: 'third_project', + creator_id: user4.id, + namespace: user4.namespace) } describe "GET /projects" do before { project } @@ -650,6 +669,118 @@ describe API::API, api: true do end end + describe 'PUT /projects/:idÍ„' do + before { project } + before { user } + before { user3 } + before { user4 } + before { project3 } + before { project4 } + before { project_member3 } + before { project_member2 } + + context 'when unauthenticated' do + it 'should return authentication error' do + project_param = { name: 'bar' } + put api("/projects/#{project.id}"), project_param + response.status.should == 401 + end + end + + context 'when authenticated as project owner' do + it 'should update name' do + project_param = { name: 'bar' } + put api("/projects/#{project.id}", user), project_param + response.status.should == 200 + project_param.each_pair do |k, v| + json_response[k.to_s].should == v + end + end + + it 'should update visibility_level' do + project_param = { visibility_level: 20 } + put api("/projects/#{project3.id}", user), project_param + response.status.should == 200 + project_param.each_pair do |k, v| + json_response[k.to_s].should == v + end + end + + it 'should not update name to existing name' do + project_param = { name: project3.name } + put api("/projects/#{project.id}", user), project_param + response.status.should == 400 + json_response['message']['name'].should == ['has already been taken'] + end + + it 'should update path & name to existing path & name in different namespace' do + project_param = { path: project4.path, name: project4.name } + put api("/projects/#{project3.id}", user), project_param + response.status.should == 200 + project_param.each_pair do |k, v| + json_response[k.to_s].should == v + end + end + end + + context 'when authenticated as project master' do + it 'should update path' do + project_param = { path: 'bar' } + put api("/projects/#{project3.id}", user4), project_param + response.status.should == 200 + project_param.each_pair do |k, v| + json_response[k.to_s].should == v + end + end + + it 'should update other attributes' do + project_param = { issues_enabled: true, + wiki_enabled: true, + snippets_enabled: true, + merge_requests_enabled: true, + description: 'new description' } + + put api("/projects/#{project3.id}", user4), project_param + response.status.should == 200 + project_param.each_pair do |k, v| + json_response[k.to_s].should == v + end + end + + it 'should not update path to existing path' do + project_param = { path: project.path } + put api("/projects/#{project3.id}", user4), project_param + response.status.should == 400 + json_response['message']['path'].should == ['has already been taken'] + end + + it 'should not update name' do + project_param = { name: 'bar' } + put api("/projects/#{project3.id}", user4), project_param + response.status.should == 403 + end + + it 'should not update visibility_level' do + project_param = { visibility_level: 20 } + put api("/projects/#{project3.id}", user4), project_param + response.status.should == 403 + end + end + + context 'when authenticated as project developer' do + it 'should not update other attributes' do + project_param = { path: 'bar', + issues_enabled: true, + wiki_enabled: true, + snippets_enabled: true, + merge_requests_enabled: true, + description: 'new description' } + put api("/projects/#{project.id}", user3), project_param + response.status.should == 403 + end + end + end + describe "DELETE /projects/:id" do context "when authenticated as user" do it "should remove project" do From 47625ab75e2cbb55ad7a7c95dcce507b3f992e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= Date: Thu, 22 Jan 2015 17:46:34 +0100 Subject: [PATCH 024/124] Fix tests and CHANGELOG entry for project edit via API --- CHANGELOG | 2 +- spec/requests/api/projects_spec.rb | 318 +++++++++++++++-------------- 2 files changed, 163 insertions(+), 157 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8468c9a7ae..634a1626b0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -54,6 +54,7 @@ v 7.8.0 - - - + - API: Add support for editing an existing project (Mika Mäenpää and Hannes Rosenögger) - - - @@ -181,7 +182,6 @@ v 7.4.0 - Fail harder in the backup script - Changes to Slack service structure, only webhook url needed - Zen mode for wiki and milestones (Robert Schilling) - - API: Add support for editing an existing project (Mika Mäenpää) - Move Emoji parsing to html-pipeline-gitlab (Robert Schilling) - Font Awesome 4.2 integration (Sullivan Senechal) - Add Pushover service integration (Sullivan Senechal) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 26d1a8d193..dc41010741 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -14,60 +14,66 @@ describe API::API, api: true do let(:project_member) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } let(:project_member2) { create(:project_member, user: user3, project: project, access_level: ProjectMember::DEVELOPER) } let(:user4) { create(:user) } - let(:project3) { create(:project, - name: 'second_project', - path: 'second_project', - creator_id: user.id, - namespace: user.namespace, - merge_requests_enabled: false, - issues_enabled: false, wiki_enabled: false, - snippets_enabled: false, visibility_level: 0) } - let(:project_member3) { create(:project_member, - user: user4, - project: project3, - access_level: ProjectMember::MASTER) } - let(:project4) { create(:project, - name: 'third_project', - path: 'third_project', - creator_id: user4.id, - namespace: user4.namespace) } + let(:project3) do + create(:project, + name: 'second_project', + path: 'second_project', + creator_id: user.id, + namespace: user.namespace, + merge_requests_enabled: false, + issues_enabled: false, wiki_enabled: false, + snippets_enabled: false, visibility_level: 0) + end + let(:project_member3) do + create(:project_member, + user: user4, + project: project3, + access_level: ProjectMember::MASTER) + end + let(:project4) do + create(:project, + name: 'third_project', + path: 'third_project', + creator_id: user4.id, + namespace: user4.namespace) + end - describe "GET /projects" do + describe 'GET /projects' do before { project } - context "when unauthenticated" do - it "should return authentication error" do - get api("/projects") + context 'when unauthenticated' do + it 'should return authentication error' do + get api('/projects') response.status.should == 401 end end - context "when authenticated" do - it "should return an array of projects" do - get api("/projects", user) + context 'when authenticated' do + it 'should return an array of projects' do + get api('/projects', user) response.status.should == 200 json_response.should be_an Array json_response.first['name'].should == project.name json_response.first['owner']['username'].should == user.username end - context "and using search" do - it "should return searched project" do - get api("/projects", user), { search: project.name } + context 'and using search' do + it 'should return searched project' do + get api('/projects', user), { search: project.name } response.status.should eq(200) json_response.should be_an Array json_response.length.should eq(1) end end - context "and using sorting" do + context 'and using sorting' do before do project2 project3 end - it "should return the correct order when sorted by id" do - get api("/projects", user), { order_by: 'id', sort: 'desc'} + it 'should return the correct order when sorted by id' do + get api('/projects', user), { order_by: 'id', sort: 'desc'} response.status.should eq(200) json_response.should be_an Array json_response.first['id'].should eq(project3.id) @@ -76,26 +82,26 @@ describe API::API, api: true do end end - describe "GET /projects/all" do + describe 'GET /projects/all' do before { project } - context "when unauthenticated" do - it "should return authentication error" do - get api("/projects/all") + context 'when unauthenticated' do + it 'should return authentication error' do + get api('/projects/all') response.status.should == 401 end end - context "when authenticated as regular user" do - it "should return authentication error" do - get api("/projects/all", user) + context 'when authenticated as regular user' do + it 'should return authentication error' do + get api('/projects/all', user) response.status.should == 403 end end - context "when authenticated as admin" do - it "should return an array of all projects" do - get api("/projects/all", admin) + context 'when authenticated as admin' do + it 'should return an array of all projects' do + get api('/projects/all', admin) response.status.should == 200 json_response.should be_an Array project_name = project.name @@ -111,59 +117,59 @@ describe API::API, api: true do end end - describe "POST /projects" do - context "maximum number of projects reached" do + describe 'POST /projects' do + context 'maximum number of projects reached' do before do (1..user2.projects_limit).each do |project| - post api("/projects", user2), name: "foo#{project}" + post api('/projects', user2), name: "foo#{project}" end end - it "should not create new project" do + it 'should not create new project' do expect { - post api("/projects", user2), name: 'foo' + post api('/projects', user2), name: 'foo' }.to change {Project.count}.by(0) end end - it "should create new project without path" do - expect { post api("/projects", user), name: 'foo' }.to change {Project.count}.by(1) + it 'should create new project without path' do + expect { post api('/projects', user), name: 'foo' }.to change {Project.count}.by(1) end - it "should not create new project without name" do - expect { post api("/projects", user) }.to_not change {Project.count} + it 'should not create new project without name' do + expect { post api('/projects', user) }.to_not change {Project.count} end - it "should return a 400 error if name not given" do - post api("/projects", user) + it 'should return a 400 error if name not given' do + post api('/projects', user) response.status.should == 400 end - it "should create last project before reaching project limit" do - (1..user2.projects_limit-1).each { |p| post api("/projects", user2), name: "foo#{p}" } - post api("/projects", user2), name: "foo" + it 'should create last project before reaching project limit' do + (1..user2.projects_limit-1).each { |p| post api('/projects', user2), name: "foo#{p}" } + post api('/projects', user2), name: 'foo' response.status.should == 201 end - it "should respond with 201 on success" do - post api("/projects", user), name: 'foo' + it 'should respond with 201 on success' do + post api('/projects', user), name: 'foo' response.status.should == 201 end - it "should respond with 400 if name is not given" do - post api("/projects", user) + it 'should respond with 400 if name is not given' do + post api('/projects', user) response.status.should == 400 end - it "should return a 403 error if project limit reached" do + it 'should return a 403 error if project limit reached' do (1..user.projects_limit).each do |p| - post api("/projects", user), name: "foo#{p}" + post api('/projects', user), name: "foo#{p}" end - post api("/projects", user), name: 'bar' + post api('/projects', user), name: 'bar' response.status.should == 403 end - it "should assign attributes to project" do + it 'should assign attributes to project' do project = attributes_for(:project, { path: 'camelCasePath', description: Faker::Lorem.sentence, @@ -172,69 +178,69 @@ describe API::API, api: true do wiki_enabled: false }) - post api("/projects", user), project + post api('/projects', user), project project.each_pair do |k,v| json_response[k.to_s].should == v end end - it "should set a project as public" do + it 'should set a project as public' do project = attributes_for(:project, :public) - post api("/projects", user), project + post api('/projects', user), project json_response['public'].should be_true json_response['visibility_level'].should == Gitlab::VisibilityLevel::PUBLIC end - it "should set a project as public using :public" do + it 'should set a project as public using :public' do project = attributes_for(:project, { public: true }) - post api("/projects", user), project + post api('/projects', user), project json_response['public'].should be_true json_response['visibility_level'].should == Gitlab::VisibilityLevel::PUBLIC end - it "should set a project as internal" do + it 'should set a project as internal' do project = attributes_for(:project, :internal) - post api("/projects", user), project + post api('/projects', user), project json_response['public'].should be_false json_response['visibility_level'].should == Gitlab::VisibilityLevel::INTERNAL end - it "should set a project as internal overriding :public" do + it 'should set a project as internal overriding :public' do project = attributes_for(:project, :internal, { public: true }) - post api("/projects", user), project + post api('/projects', user), project json_response['public'].should be_false json_response['visibility_level'].should == Gitlab::VisibilityLevel::INTERNAL end - it "should set a project as private" do + it 'should set a project as private' do project = attributes_for(:project, :private) - post api("/projects", user), project + post api('/projects', user), project json_response['public'].should be_false json_response['visibility_level'].should == Gitlab::VisibilityLevel::PRIVATE end - it "should set a project as private using :public" do + it 'should set a project as private using :public' do project = attributes_for(:project, { public: false }) - post api("/projects", user), project + post api('/projects', user), project json_response['public'].should be_false json_response['visibility_level'].should == Gitlab::VisibilityLevel::PRIVATE end end - describe "POST /projects/user/:id" do + describe 'POST /projects/user/:id' do before { project } before { admin } - it "should create new project without path" do + it 'should create new project without path' do expect { post api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1) end - it "should not create new project without name" do + it 'should not create new project without name' do expect { post api("/projects/user/#{user.id}", admin) }.to_not change {Project.count} end - it "should respond with 201 on success" do + it 'should respond with 201 on success' do post api("/projects/user/#{user.id}", admin), name: 'foo' response.status.should == 201 end @@ -254,7 +260,7 @@ describe API::API, api: true do ] end - it "should assign attributes to project" do + it 'should assign attributes to project' do project = attributes_for(:project, { description: Faker::Lorem.sentence, issues_enabled: false, @@ -270,42 +276,42 @@ describe API::API, api: true do end end - it "should set a project as public" do + it 'should set a project as public' do project = attributes_for(:project, :public) post api("/projects/user/#{user.id}", admin), project json_response['public'].should be_true json_response['visibility_level'].should == Gitlab::VisibilityLevel::PUBLIC end - it "should set a project as public using :public" do + it 'should set a project as public using :public' do project = attributes_for(:project, { public: true }) post api("/projects/user/#{user.id}", admin), project json_response['public'].should be_true json_response['visibility_level'].should == Gitlab::VisibilityLevel::PUBLIC end - it "should set a project as internal" do + it 'should set a project as internal' do project = attributes_for(:project, :internal) post api("/projects/user/#{user.id}", admin), project json_response['public'].should be_false json_response['visibility_level'].should == Gitlab::VisibilityLevel::INTERNAL end - it "should set a project as internal overriding :public" do + it 'should set a project as internal overriding :public' do project = attributes_for(:project, :internal, { public: true }) post api("/projects/user/#{user.id}", admin), project json_response['public'].should be_false json_response['visibility_level'].should == Gitlab::VisibilityLevel::INTERNAL end - it "should set a project as private" do + it 'should set a project as private' do project = attributes_for(:project, :private) post api("/projects/user/#{user.id}", admin), project json_response['public'].should be_false json_response['visibility_level'].should == Gitlab::VisibilityLevel::PRIVATE end - it "should set a project as private using :public" do + it 'should set a project as private using :public' do project = attributes_for(:project, { public: false }) post api("/projects/user/#{user.id}", admin), project json_response['public'].should be_false @@ -313,30 +319,30 @@ describe API::API, api: true do end end - describe "GET /projects/:id" do + describe 'GET /projects/:id' do before { project } before { project_member } - it "should return a project by id" do + it 'should return a project by id' do get api("/projects/#{project.id}", user) response.status.should == 200 json_response['name'].should == project.name json_response['owner']['username'].should == user.username end - it "should return a project by path name" do + it 'should return a project by path name' do get api("/projects/#{project.id}", user) response.status.should == 200 json_response['name'].should == project.name end - it "should return a 404 error if not found" do - get api("/projects/42", user) + it 'should return a 404 error if not found' do + get api('/projects/42', user) response.status.should == 404 json_response['message'].should == '404 Project Not Found' end - it "should return a 404 error if user is not a member" do + it 'should return a 404 error if user is not a member' do other_user = create(:user) get api("/projects/#{project.id}", other_user) response.status.should == 404 @@ -350,8 +356,8 @@ describe API::API, api: true do end it { response.status.should == 200 } - it { json_response['permissions']["project_access"]["access_level"].should == Gitlab::Access::MASTER } - it { json_response['permissions']["group_access"].should be_nil } + it { json_response['permissions']['project_access']['access_level'].should == Gitlab::Access::MASTER } + it { json_response['permissions']['group_access'].should be_nil } end context 'group project' do @@ -362,16 +368,16 @@ describe API::API, api: true do end it { response.status.should == 200 } - it { json_response['permissions']["project_access"].should be_nil } - it { json_response['permissions']["group_access"]["access_level"].should == Gitlab::Access::OWNER } + it { json_response['permissions']['project_access'].should be_nil } + it { json_response['permissions']['group_access']['access_level'].should == Gitlab::Access::OWNER } end end end - describe "GET /projects/:id/events" do + describe 'GET /projects/:id/events' do before { project_member } - it "should return a project events" do + it 'should return a project events' do get api("/projects/#{project.id}/events", user) response.status.should == 200 json_event = json_response.first @@ -381,23 +387,23 @@ describe API::API, api: true do json_event['author_username'].should == user.username end - it "should return a 404 error if not found" do - get api("/projects/42/events", user) + it 'should return a 404 error if not found' do + get api('/projects/42/events', user) response.status.should == 404 json_response['message'].should == '404 Project Not Found' end - it "should return a 404 error if user is not a member" do + it 'should return a 404 error if user is not a member' do other_user = create(:user) get api("/projects/#{project.id}/events", other_user) response.status.should == 404 end end - describe "GET /projects/:id/snippets" do + describe 'GET /projects/:id/snippets' do before { snippet } - it "should return an array of project snippets" do + it 'should return an array of project snippets' do get api("/projects/#{project.id}/snippets", user) response.status.should == 200 json_response.should be_an Array @@ -405,48 +411,48 @@ describe API::API, api: true do end end - describe "GET /projects/:id/snippets/:snippet_id" do - it "should return a project snippet" do + describe 'GET /projects/:id/snippets/:snippet_id' do + it 'should return a project snippet' do get api("/projects/#{project.id}/snippets/#{snippet.id}", user) response.status.should == 200 json_response['title'].should == snippet.title end - it "should return a 404 error if snippet id not found" do + it 'should return a 404 error if snippet id not found' do get api("/projects/#{project.id}/snippets/1234", user) response.status.should == 404 end end - describe "POST /projects/:id/snippets" do - it "should create a new project snippet" do + describe 'POST /projects/:id/snippets' do + it 'should create a new project snippet' do post api("/projects/#{project.id}/snippets", user), title: 'api test', file_name: 'sample.rb', code: 'test' response.status.should == 201 json_response['title'].should == 'api test' end - it "should return a 400 error if title is not given" do + it 'should return a 400 error if title is not given' do post api("/projects/#{project.id}/snippets", user), file_name: 'sample.rb', code: 'test' response.status.should == 400 end - it "should return a 400 error if file_name not given" do + it 'should return a 400 error if file_name not given' do post api("/projects/#{project.id}/snippets", user), title: 'api test', code: 'test' response.status.should == 400 end - it "should return a 400 error if code not given" do + it 'should return a 400 error if code not given' do post api("/projects/#{project.id}/snippets", user), title: 'api test', file_name: 'sample.rb' response.status.should == 400 end end - describe "PUT /projects/:id/snippets/:shippet_id" do - it "should update an existing project snippet" do + describe 'PUT /projects/:id/snippets/:shippet_id' do + it 'should update an existing project snippet' do put api("/projects/#{project.id}/snippets/#{snippet.id}", user), code: 'updated code' response.status.should == 200 @@ -454,7 +460,7 @@ describe API::API, api: true do snippet.reload.content.should == 'updated code' end - it "should update an existing project snippet with new title" do + it 'should update an existing project snippet with new title' do put api("/projects/#{project.id}/snippets/#{snippet.id}", user), title: 'other api test' response.status.should == 200 @@ -462,10 +468,10 @@ describe API::API, api: true do end end - describe "DELETE /projects/:id/snippets/:snippet_id" do + describe 'DELETE /projects/:id/snippets/:snippet_id' do before { snippet } - it "should delete existing project snippet" do + it 'should delete existing project snippet' do expect { delete api("/projects/#{project.id}/snippets/#{snippet.id}", user) }.to change { Snippet.count }.by(-1) @@ -478,13 +484,13 @@ describe API::API, api: true do end end - describe "GET /projects/:id/snippets/:snippet_id/raw" do - it "should get a raw project snippet" do + describe 'GET /projects/:id/snippets/:snippet_id/raw' do + it 'should get a raw project snippet' do get api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user) response.status.should == 200 end - it "should return a 404 error if raw project snippet not found" do + it 'should return a 404 error if raw project snippet not found' do get api("/projects/#{project.id}/snippets/5555/raw", user) response.status.should == 404 end @@ -494,10 +500,10 @@ describe API::API, api: true do let(:deploy_keys_project) { create(:deploy_keys_project, project: project) } let(:deploy_key) { deploy_keys_project.deploy_key } - describe "GET /projects/:id/keys" do + describe 'GET /projects/:id/keys' do before { deploy_key } - it "should return array of ssh keys" do + it 'should return array of ssh keys' do get api("/projects/#{project.id}/keys", user) response.status.should == 200 json_response.should be_an Array @@ -505,22 +511,22 @@ describe API::API, api: true do end end - describe "GET /projects/:id/keys/:key_id" do - it "should return a single key" do + describe 'GET /projects/:id/keys/:key_id' do + it 'should return a single key' do get api("/projects/#{project.id}/keys/#{deploy_key.id}", user) response.status.should == 200 json_response['title'].should == deploy_key.title end - it "should return 404 Not Found with invalid ID" do + it 'should return 404 Not Found with invalid ID' do get api("/projects/#{project.id}/keys/404", user) response.status.should == 404 end end - describe "POST /projects/:id/keys" do - it "should not create an invalid ssh key" do - post api("/projects/#{project.id}/keys", user), { title: "invalid key" } + describe 'POST /projects/:id/keys' do + it 'should not create an invalid ssh key' do + post api("/projects/#{project.id}/keys", user), { title: 'invalid key' } response.status.should == 400 json_response['message']['key'].should == [ 'can\'t be blank', @@ -538,7 +544,7 @@ describe API::API, api: true do ] end - it "should create new ssh key" do + it 'should create new ssh key' do key_attrs = attributes_for :key expect { post api("/projects/#{project.id}/keys", user), key_attrs @@ -546,16 +552,16 @@ describe API::API, api: true do end end - describe "DELETE /projects/:id/keys/:key_id" do + describe 'DELETE /projects/:id/keys/:key_id' do before { deploy_key } - it "should delete existing key" do + it 'should delete existing key' do expect { delete api("/projects/#{project.id}/keys/#{deploy_key.id}", user) }.to change{ project.deploy_keys.count }.by(-1) end - it "should return 404 Not Found with invalid ID" do + it 'should return 404 Not Found with invalid ID' do delete api("/projects/#{project.id}/keys/404", user) response.status.should == 404 end @@ -566,7 +572,7 @@ describe API::API, api: true do let(:project_fork_target) { create(:project) } let(:project_fork_source) { create(:project, :public) } - describe "POST /projects/:id/fork/:forked_from_id" do + describe 'POST /projects/:id/fork/:forked_from_id' do let(:new_project_fork_source) { create(:project, :public) } it "shouldn't available for non admin users" do @@ -574,7 +580,7 @@ describe API::API, api: true do response.status.should == 403 end - it "should allow project to be forked from an existing project" do + it 'should allow project to be forked from an existing project' do project_fork_target.forked?.should_not be_true post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) response.status.should == 201 @@ -584,12 +590,12 @@ describe API::API, api: true do project_fork_target.forked?.should be_true end - it "should fail if forked_from project which does not exist" do + it 'should fail if forked_from project which does not exist' do post api("/projects/#{project_fork_target.id}/fork/9999", admin) response.status.should == 404 end - it "should fail with 409 if already forked" do + it 'should fail with 409 if already forked' do post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) project_fork_target.reload project_fork_target.forked_from_project.id.should == project_fork_source.id @@ -601,14 +607,14 @@ describe API::API, api: true do end end - describe "DELETE /projects/:id/fork" do + describe 'DELETE /projects/:id/fork' do it "shouldn't available for non admin users" do delete api("/projects/#{project_fork_target.id}/fork", user) response.status.should == 403 end - it "should make forked project unforked" do + it 'should make forked project unforked' do post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) project_fork_target.reload project_fork_target.forked_from_project.should_not be_nil @@ -620,7 +626,7 @@ describe API::API, api: true do project_fork_target.forked?.should_not be_true end - it "should be idempotent if not forked" do + it 'should be idempotent if not forked' do project_fork_target.forked_from_project.should be_nil delete api("/projects/#{project_fork_target.id}/fork", admin) response.status.should == 200 @@ -629,7 +635,7 @@ describe API::API, api: true do end end - describe "GET /projects/search/:query" do + describe 'GET /projects/search/:query' do let!(:query) { 'query'} let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) } let!(:pre) { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) } @@ -641,15 +647,15 @@ describe API::API, api: true do let!(:public) { create(:empty_project, :public, name: "public #{query}") } let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') } - context "when unauthenticated" do - it "should return authentication error" do + context 'when unauthenticated' do + it 'should return authentication error' do get api("/projects/search/#{query}") response.status.should == 401 end end - context "when authenticated" do - it "should return an array of projects" do + context 'when authenticated' do + it 'should return an array of projects' do get api("/projects/search/#{query}",user) response.status.should == 200 json_response.should be_an Array @@ -658,8 +664,8 @@ describe API::API, api: true do end end - context "when authenticated as a different user" do - it "should return matching public projects" do + context 'when authenticated as a different user' do + it 'should return matching public projects' do get api("/projects/search/#{query}", user2) response.status.should == 200 json_response.should be_an Array @@ -781,9 +787,9 @@ describe API::API, api: true do end end - describe "DELETE /projects/:id" do - context "when authenticated as user" do - it "should remove project" do + describe 'DELETE /projects/:id' do + context 'when authenticated as user' do + it 'should remove project' do expect(GitlabShellWorker).to( receive(:perform_async).with(:remove_repository, /#{project.path_with_namespace}/) @@ -793,32 +799,32 @@ describe API::API, api: true do response.status.should == 200 end - it "should not remove a project if not an owner" do + it 'should not remove a project if not an owner' do user3 = create(:user) project.team << [user3, :developer] delete api("/projects/#{project.id}", user3) response.status.should == 403 end - it "should not remove a non existing project" do - delete api("/projects/1328", user) + it 'should not remove a non existing project' do + delete api('/projects/1328', user) response.status.should == 404 end - it "should not remove a project not attached to user" do + it 'should not remove a project not attached to user' do delete api("/projects/#{project.id}", user2) response.status.should == 404 end end - context "when authenticated as admin" do - it "should remove any existing project" do + context 'when authenticated as admin' do + it 'should remove any existing project' do delete api("/projects/#{project.id}", admin) response.status.should == 200 end - it "should not remove a non existing project" do - delete api("/projects/1328", admin) + it 'should not remove a non existing project' do + delete api('/projects/1328', admin) response.status.should == 404 end end From a5b255fbdfcd01f355de2dca76e379e12b43d6e6 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 22 Jan 2015 09:37:47 -0800 Subject: [PATCH 025/124] Code improvements according to styleguide --- app/models/concerns/issuable.rb | 8 ++++++-- app/models/note.rb | 7 +++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index b8bee0d0ec..fb038a3cc3 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -124,10 +124,12 @@ module Issuable users << assignee if is_assigned? mentions = [] mentions << self.mentioned_users + notes.each do |note| users << note.author mentions << note.mentioned_users end + users.concat(mentions.reduce([], :|)).uniq end @@ -149,8 +151,8 @@ module Issuable def add_labels_by_names(label_names) label_names.each do |label_name| - label = project.labels.create_with( - color: Label::DEFAULT_COLOR).find_or_create_by(title: label_name.strip) + label = project.labels.create_with(color: Label::DEFAULT_COLOR). + find_or_create_by(title: label_name.strip) self.labels << label end end @@ -159,11 +161,13 @@ module Issuable def filter_superceded_votes(votes, notes) filteredvotes = [] + votes + votes.each do |vote| if vote.superceded?(notes) filteredvotes.delete(vote) end end + filteredvotes end end diff --git a/app/models/note.rb b/app/models/note.rb index cb879dc2ce..0b988cc3e0 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -461,14 +461,17 @@ class Note < ActiveRecord::Base def superceded?(notes) return false unless vote? + notes.each do |note| next if note == self + if note.vote? && - self[:author_id] == note[:author_id] && - self[:created_at] <= note[:created_at] + self[:author_id] == note[:author_id] && + self[:created_at] <= note[:created_at] return true end end + false end From 98ee4a1fa73183cacf6c470b56e34afccec1c5dc Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 22 Jan 2015 09:40:03 -0800 Subject: [PATCH 026/124] Annotate models --- app/models/application_setting.rb | 15 +++++++++++++++ app/models/identity.rb | 12 +++++++++++- app/models/merge_request.rb | 1 + app/models/project.rb | 2 ++ app/models/project_services/bamboo_service.rb | 14 ++++++++++++++ app/models/project_services/teamcity_service.rb | 14 ++++++++++++++ app/models/protected_branch.rb | 11 ++++++----- app/models/user.rb | 5 ++--- spec/factories/merge_requests.rb | 1 + spec/factories/projects.rb | 2 ++ spec/models/application_setting_spec.rb | 15 +++++++++++++++ spec/models/merge_request_spec.rb | 1 + spec/models/project_spec.rb | 2 ++ spec/models/protected_branch_spec.rb | 11 ++++++----- spec/models/user_spec.rb | 5 ++--- 15 files changed, 94 insertions(+), 17 deletions(-) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index aed4068f30..45ae79a75c 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -1,3 +1,18 @@ +# == Schema Information +# +# Table name: application_settings +# +# id :integer not null, primary key +# default_projects_limit :integer +# signup_enabled :boolean +# signin_enabled :boolean +# gravatar_enabled :boolean +# sign_in_text :text +# created_at :datetime +# updated_at :datetime +# home_page_url :string(255) +# + class ApplicationSetting < ActiveRecord::Base validates :home_page_url, allow_blank: true, format: { with: URI::regexp(%w(http https)), message: "should be a valid url" }, diff --git a/app/models/identity.rb b/app/models/identity.rb index 5fb1850c30..c7cdb63e3d 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -1,5 +1,15 @@ +# == Schema Information +# +# Table name: identities +# +# id :integer not null, primary key +# extern_uid :string(255) +# provider :string(255) +# user_id :integer +# + class Identity < ActiveRecord::Base belongs_to :user validates :extern_uid, allow_blank: true, uniqueness: {scope: :provider} -end \ No newline at end of file +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index ae6f88c2e6..715257f905 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -18,6 +18,7 @@ # iid :integer # description :text # position :integer default(0) +# locked_at :datetime # require Rails.root.join("app/models/commit") diff --git a/app/models/project.rb b/app/models/project.rb index 4785199b14..f102c47740 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -24,6 +24,8 @@ # import_status :string(255) # repository_size :float default(0.0) # star_count :integer default(0), not null +# import_type :string(255) +# import_source :string(255) # class Project < ActiveRecord::Base diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index b9eec9ab21..16e1b83da4 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -1,3 +1,17 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + class BambooService < CiService include HTTParty diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 52b5862e4d..dca718b5e8 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -1,3 +1,17 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# + class TeamcityService < CiService include HTTParty diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 1b06dd7752..97207ba127 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -2,11 +2,12 @@ # # Table name: protected_branches # -# id :integer not null, primary key -# project_id :integer not null -# name :string(255) not null -# created_at :datetime -# updated_at :datetime +# id :integer not null, primary key +# project_id :integer not null +# name :string(255) not null +# created_at :datetime +# updated_at :datetime +# developers_can_push :boolean default(FALSE), not null # class ProtectedBranch < ActiveRecord::Base diff --git a/app/models/user.rb b/app/models/user.rb index 852f3fc48c..69fe674df8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,8 +26,6 @@ # bio :string(255) # failed_attempts :integer default(0) # locked_at :datetime -# extern_uid :string(255) -# provider :string(255) # username :string(255) # can_create_group :boolean default(TRUE), not null # can_create_team :boolean default(TRUE), not null @@ -36,7 +34,6 @@ # notification_level :integer default(1), not null # password_expires_at :datetime # created_by_id :integer -# last_credential_check_at :datetime # avatar :string(255) # confirmation_token :string(255) # confirmed_at :datetime @@ -44,6 +41,8 @@ # unconfirmed_email :string(255) # hide_no_ssh_key :boolean default(FALSE) # website_url :string(255) default(""), not null +# last_credential_check_at :datetime +# github_access_token :string(255) # require 'carrierwave/orm/activerecord' diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 0ae8ea5f87..6ce1d7446f 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -18,6 +18,7 @@ # iid :integer # description :text # position :integer default(0) +# locked_at :datetime # FactoryGirl.define do diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 60eb73e4a9..1738b20fab 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -24,6 +24,8 @@ # import_status :string(255) # repository_size :float default(0.0) # star_count :integer default(0), not null +# import_type :string(255) +# import_source :string(255) # FactoryGirl.define do diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 039775dddd..1723eba9ec 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -1,3 +1,18 @@ +# == Schema Information +# +# Table name: application_settings +# +# id :integer not null, primary key +# default_projects_limit :integer +# signup_enabled :boolean +# signin_enabled :boolean +# gravatar_enabled :boolean +# sign_in_text :text +# created_at :datetime +# updated_at :datetime +# home_page_url :string(255) +# + require 'spec_helper' describe ApplicationSetting, models: true do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 7b0d261d72..9585cf0976 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -18,6 +18,7 @@ # iid :integer # description :text # position :integer default(0) +# locked_at :datetime # require 'spec_helper' diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 70a15cac1a..2a27817637 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -24,6 +24,8 @@ # import_status :string(255) # repository_size :float default(0.0) # star_count :integer default(0), not null +# import_type :string(255) +# import_source :string(255) # require 'spec_helper' diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb index af48c2c6d9..b0f57e8a20 100644 --- a/spec/models/protected_branch_spec.rb +++ b/spec/models/protected_branch_spec.rb @@ -2,11 +2,12 @@ # # Table name: protected_branches # -# id :integer not null, primary key -# project_id :integer not null -# name :string(255) not null -# created_at :datetime -# updated_at :datetime +# id :integer not null, primary key +# project_id :integer not null +# name :string(255) not null +# created_at :datetime +# updated_at :datetime +# developers_can_push :boolean default(FALSE), not null # require 'spec_helper' diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 8be7f733a5..83341e516a 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -26,8 +26,6 @@ # bio :string(255) # failed_attempts :integer default(0) # locked_at :datetime -# extern_uid :string(255) -# provider :string(255) # username :string(255) # can_create_group :boolean default(TRUE), not null # can_create_team :boolean default(TRUE), not null @@ -36,7 +34,6 @@ # notification_level :integer default(1), not null # password_expires_at :datetime # created_by_id :integer -# last_credential_check_at :datetime # avatar :string(255) # confirmation_token :string(255) # confirmed_at :datetime @@ -44,6 +41,8 @@ # unconfirmed_email :string(255) # hide_no_ssh_key :boolean default(FALSE) # website_url :string(255) default(""), not null +# last_credential_check_at :datetime +# github_access_token :string(255) # require 'spec_helper' From d005c44816b87637b5952dd50178fb7e94e30cc2 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 22 Jan 2015 10:38:24 -0800 Subject: [PATCH 027/124] Mix wrong comment about signup --- config/gitlab.yml.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 8d97965935..e5780cabb6 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -60,7 +60,7 @@ production: &base ## Users can create accounts # This also allows normal users to sign up for accounts themselves - # default: false - By default GitLab administrators must create all new accounts + # default: true - By default users can sign up themselves # signup_enabled: true ## Standard login settings From 7afa4d5791bc9cf94eb3e33257d7f6f740c6bcf1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 22 Jan 2015 11:14:29 -0800 Subject: [PATCH 028/124] Improve commit title --- app/assets/stylesheets/sections/commits.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/sections/commits.scss b/app/assets/stylesheets/sections/commits.scss index 2fd2dcba47..2e274d06c1 100644 --- a/app/assets/stylesheets/sections/commits.scss +++ b/app/assets/stylesheets/sections/commits.scss @@ -101,7 +101,6 @@ .commit-title { margin: 0; - font-size: 20px; } .commit-description { From 7411fb36c5fc3b938e8b8228d217e645c9768a93 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 22 Jan 2015 11:15:21 -0800 Subject: [PATCH 029/124] Show branches/tags as labels on commit page --- app/helpers/commits_helper.rb | 18 +++++++++-- .../projects/commit/_commit_box.html.haml | 30 +++++++++---------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 8214df4007..2a3e51ada5 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -62,13 +62,27 @@ module CommitsHelper # Returns the sorted alphabetically links to branches, separated by a comma def commit_branches_links(project, branches) - branches.sort.map { |branch| link_to(branch, project_tree_path(project, branch)) }.join(", ").html_safe + branches.sort.map do |branch| + link_to(project_tree_path(project, branch)) do + content_tag :span, class: 'label label-gray' do + content_tag(:i, nil, class: 'fa fa-code-fork') + ' ' + + branch + end + end + end.join(" ").html_safe end # Returns the sorted links to tags, separated by a comma def commit_tags_links(project, tags) sorted = VersionSorter.rsort(tags) - sorted.map { |tag| link_to(tag, project_commits_path(project, project.repository.find_tag(tag).name)) }.join(", ").html_safe + sorted.map do |tag| + link_to(project_commits_path(project, project.repository.find_tag(tag).name)) do + content_tag :span, class: 'label label-gray' do + content_tag(:i, nil, class: 'fa fa-tag') + ' ' + + tag + end + end + end.join(" ").html_safe end def link_to_browse_code(project, commit) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 1d4658432a..b41fb1437f 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -37,25 +37,23 @@ - @commit.parents.each do |parent| = link_to parent.short_id, project_commit_path(@project, parent) -- if @branches.any? - .commit-info-row - %span.cgray - Exists in +.commit-info-row + - if @branches.any? %span - branch = commit_default_branch(@project, @branches) - = link_to(branch, project_tree_path(@project, branch)) - - if @branches.any? - and in - = link_to("#{pluralize(@branches.count, "other branch")}", "#", class: "js-details-expand") + = link_to(project_tree_path(@project, branch)) do + %span.label.label-gray + %i.fa.fa-code-fork + = branch + - if @branches.any? || @tags.any? + = link_to("#", class: "js-details-expand") do + %span.label.label-gray + \... %span.js-details-content.hide - = commit_branches_links(@project, @branches) - -- if @tags.any? - .commit-info-row - %span.cgray - Tags: - %span - = commit_tags_links(@project, @tags) + - if @branches.any? + = commit_branches_links(@project, @branches) + - if @tags.any? + = commit_tags_links(@project, @tags) .commit-box %h3.commit-title From a5f1b67ed4d5c46d1c243260cbfe86733fee075e Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 22 Jan 2015 11:24:05 -0800 Subject: [PATCH 030/124] Dont allow image overflow in comment form and timeline --- app/assets/stylesheets/generic/timeline.scss | 4 ++++ app/assets/stylesheets/sections/note_form.scss | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/assets/stylesheets/generic/timeline.scss b/app/assets/stylesheets/generic/timeline.scss index 82ee41b71b..ee3bf9c1b2 100644 --- a/app/assets/stylesheets/generic/timeline.scss +++ b/app/assets/stylesheets/generic/timeline.scss @@ -58,6 +58,10 @@ padding: 10px 15px; margin-left: 60px; + img { + max-width: 100%; + } + &:after { content: ''; display: block; diff --git a/app/assets/stylesheets/sections/note_form.scss b/app/assets/stylesheets/sections/note_form.scss index 26511d799f..61a877a5e4 100644 --- a/app/assets/stylesheets/sections/note_form.scss +++ b/app/assets/stylesheets/sections/note_form.scss @@ -52,6 +52,10 @@ } } + img { + max-width: 100%; + } + .note_text { width: 100%; } From abbb29b30c55fb44edd4f1eb0b87a8ce4a72a05d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= Date: Thu, 22 Jan 2015 20:27:11 +0100 Subject: [PATCH 031/124] Update ssh doc with commands to copy the public key to the clipboard --- doc/ssh/ssh.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/doc/ssh/ssh.md b/doc/ssh/ssh.md index d466c1bde7..f9ee627f1f 100644 --- a/doc/ssh/ssh.md +++ b/doc/ssh/ssh.md @@ -18,4 +18,21 @@ Use the code below to show your public key. cat ~/.ssh/id_rsa.pub ``` -Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your user profile. Please copy the complete key starting with `ssh-` and ending with your username and host. +Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your user profile. Please copy the complete key starting with `ssh-` and ending with your username and host. + +Use code below to copy your public key to the clipboard. Depending on your OS you'll need to use a different command: + +**Windows:** +```bash +clip < ~/.ssh/id_rsa.pub +``` + +**Mac:** +```bash +pbcopy < ~/.ssh/id_rsa.pub +``` + +**Linux (requires xclip):** +```bash +xclip -sel clip < ~/.ssh/id_rsa.pub +``` From 130663e7ad099f057c19df9a024e029e182501d8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 22 Jan 2015 11:48:20 -0800 Subject: [PATCH 032/124] Show modal window with instructions if GH OAuth is not enables --- .../projects/_github_import_modal.html.haml | 22 +++++++++++++++++++ app/views/projects/new.html.haml | 13 +++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 app/views/projects/_github_import_modal.html.haml diff --git a/app/views/projects/_github_import_modal.html.haml b/app/views/projects/_github_import_modal.html.haml new file mode 100644 index 0000000000..02c9ef45f2 --- /dev/null +++ b/app/views/projects/_github_import_modal.html.haml @@ -0,0 +1,22 @@ +%div#github_import_modal.modal.hide + .modal-dialog + .modal-content + .modal-header + %a.close{href: "#", "data-dismiss" => "modal"} Ă— + %h3 GitHub OAuth import + .modal-body + You need to setup integration with GitHub first. + = link_to 'How to setup integration with GitHub', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/github.md' + + +:javascript + $(function(){ + var import_modal = $('#github_import_modal').modal({modal: true, show:false}); + $('.how_to_import_link').bind("click", function(e){ + e.preventDefault(); + import_modal.show(); + }); + $('.modal-header .close').bind("click", function(){ + import_modal.hide(); + }) + }) diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index ccd02acd76..3e0f9cbd80 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -40,13 +40,18 @@ The import will time out after 4 minutes. For big repositories, use a clone/push combination. For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"} - - if github_import_enabled? - .project-import.form-group - .col-sm-2 - .col-sm-10 + .project-import.form-group + .col-sm-2 + .col-sm-10 + - if github_import_enabled? = link_to status_github_import_path do %i.fa.fa-github Import projects from GitHub + - else + = link_to '#', class: 'how_to_import_link light' do + %i.fa.fa-github + Import projects from GitHub + = render 'github_import_modal' %hr.prepend-botton-10 From 3395457acbc8ec898edae622eedc20c8fcd6b8fa Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 22 Jan 2015 12:08:47 -0800 Subject: [PATCH 033/124] Fix vote specs for mysql --- spec/lib/votes_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/lib/votes_spec.rb b/spec/lib/votes_spec.rb index 63692814b9..2c01a34756 100644 --- a/spec/lib/votes_spec.rb +++ b/spec/lib/votes_spec.rb @@ -178,7 +178,8 @@ describe Issue, 'Votes' do end def add_note(text, author = issue.author) + created_at = Time.now - 1.hour + Note.count.seconds issue.notes << create(:note, note: text, project: issue.project, - author_id: author.id) + author_id: author.id, created_at: created_at) end end From 4ade9bf24da50091e3a3fc310ca9070caed19a75 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 22 Jan 2015 17:05:31 -0800 Subject: [PATCH 034/124] lighter hover color --- app/assets/stylesheets/main/variables.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/main/variables.scss b/app/assets/stylesheets/main/variables.scss index 0f2c063297..32fde32c42 100644 --- a/app/assets/stylesheets/main/variables.scss +++ b/app/assets/stylesheets/main/variables.scss @@ -2,7 +2,7 @@ * General Colors */ $style_color: #474D57; -$hover: #FFECDB; +$hover: #FFF3EB; $box_bg: #F9F9F9; /* From 1511999d88d4a6a7c8a42f93dff93097b92682b1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 22 Jan 2015 17:51:35 -0800 Subject: [PATCH 035/124] Faster autocomplete for users/issues/emojiis Instead of loading all issues and merge requests we load only open one. This will reduce time load for autocomplete resources significantly --- app/controllers/projects_controller.rb | 15 ++++++++++++--- app/services/projects/autocomplete_service.rb | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 app/services/projects/autocomplete_service.rb diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index e541b6fd87..7fc283ef3d 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -101,11 +101,20 @@ class ProjectsController < ApplicationController def autocomplete_sources note_type = params['type'] note_id = params['type_id'] + autocomplete = ::Projects::AutocompleteService.new(@project) participants = ::Projects::ParticipantsService.new(@project).execute(note_type, note_id) + + emojis = Emoji.names.map do |e| + { + name: e, + path: view_context.image_url("emoji/#{e}.png") + } + end + @suggestions = { - emojis: Emoji.names.map { |e| { name: e, path: view_context.image_url("emoji/#{e}.png") } }, - issues: @project.issues.select([:iid, :title, :description]), - mergerequests: @project.merge_requests.select([:iid, :title, :description]), + emojis: emojis, + issues: autocomplete.issues, + mergerequests: autocomplete.merge_requests, members: participants } diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb new file mode 100644 index 0000000000..09fc25cc1b --- /dev/null +++ b/app/services/projects/autocomplete_service.rb @@ -0,0 +1,15 @@ +module Projects + class AutocompleteService < BaseService + def initialize(project) + @project = project + end + + def issues + @project.issues.opened.select([:iid, :title, :description]) + end + + def merge_requests + @project.merge_requests.opened.select([:iid, :title, :description]) + end + end +end From e36334c77071b565f6d533bc1dcb2ecf78e6b7cc Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Thu, 22 Jan 2015 18:39:05 -0800 Subject: [PATCH 036/124] allow to use http in redirect url --- config/initializers/doorkeeper.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 23d9852725..4819ab273d 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -36,6 +36,12 @@ Doorkeeper.configure do # Issue access tokens with refresh token (disabled by default) use_refresh_token + # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled + # by default in non-development environments). OAuth2 delegates security in + # communication to the HTTPS protocol so it is wise to keep this enabled. + # + force_ssl_in_redirect_uri false + # Provide support for an owner to be assigned to each registered application (disabled by default) # Optional parameter :confirmation => true (default false) if you want to enforce ownership of # a registered application From 22b7d2156c6d67deb85eccba5ca7e542a7f409e6 Mon Sep 17 00:00:00 2001 From: Hiroyuki Sato Date: Fri, 23 Jan 2015 20:36:53 +0900 Subject: [PATCH 037/124] Remember the project default tab for 30days --- app/assets/javascripts/project_show.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/project_show.js.coffee b/app/assets/javascripts/project_show.js.coffee index 02a7d7b731..d0eaaad92b 100644 --- a/app/assets/javascripts/project_show.js.coffee +++ b/app/assets/javascripts/project_show.js.coffee @@ -6,7 +6,7 @@ class @ProjectShow new Flash('Star toggle failed. Try again later.', 'alert') $("a[data-toggle='tab']").on "shown.bs.tab", (e) -> - $.cookie "default_view", $(e.target).attr("href") + $.cookie "default_view", $(e.target).attr("href"), { expires: 30 } defaultView = $.cookie("default_view") if defaultView From 737f6516e697ec5876fcdeb55acedfeefd24c9cc Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 23 Jan 2015 09:14:45 -0800 Subject: [PATCH 038/124] Update new services with initialization based on existing data. --- .../project_services/custom_issue_tracker_service.rb | 2 +- app/models/project_services/jira_service.rb | 12 +++++++++++- app/models/project_services/redmine_service.rb | 11 ++++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index 69e1b204ba..2476b62da8 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -19,7 +19,7 @@ class CustomIssueTrackerService < IssueTrackerService end def to_param - title.parameterize + 'custom_issue_tracker' end def fields diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index f83f01c55b..f8b04ddeea 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -37,7 +37,7 @@ class JiraService < IssueTrackerService if enabled_in_gitlab_config self.properties = { title: issues_tracker['title'], - project_url: issues_tracker['project_url'], + project_url: set_project_url, issues_url: issues_tracker['issues_url'], new_issue_url: issues_tracker['new_issue_url'] } @@ -56,4 +56,14 @@ class JiraService < IssueTrackerService def issues_tracker Gitlab.config.issues_tracker['jira'] end + + def set_project_url + id = self.project.issues_tracker_id + + if id + issues_tracker['project_url'].gsub(":issues_tracker_id", id) + else + issues_tracker['project_url'] + end + end end diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index 55841b5005..03f7115d84 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -27,7 +27,7 @@ class RedmineService < IssueTrackerService if enabled_in_gitlab_config self.properties = { title: issues_tracker['title'], - project_url: issues_tracker['project_url'], + project_url: set_project_url, issues_url: issues_tracker['issues_url'], new_issue_url: issues_tracker['new_issue_url'] } @@ -46,4 +46,13 @@ class RedmineService < IssueTrackerService def issues_tracker Gitlab.config.issues_tracker['redmine'] end + + def set_project_url + id = self.project.issue_tracker_id + if id + issues_tracker['project_url'].gsub(":issue_tracker_id", id) + else + issues_tracker['project_url'] + end + end end From c6e24850a3ad662d82f8e0812eb2a38df4f43c13 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Jan 2015 09:38:11 -0800 Subject: [PATCH 039/124] Update CHANGELOG with 7.7.1 --- CHANGELOG | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 36cc6052d8..bd51900253 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -60,6 +60,11 @@ v 7.8.0 - - +v 7.7.1 + - Improve mention autocomplete performance + - Show setup instructions for GitHub import if disabled + - Allow use http for OAuth applications + v 7.7.0 - Import from GitHub.com feature - Add Jetbrains Teamcity CI service (Jason Lippert) From 103a1bb06d00c0b3ee1f0148ee8fc809f4f276f8 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 23 Jan 2015 10:28:38 -0800 Subject: [PATCH 040/124] Use service settings instead of config file settings to present issues. --- app/helpers/issues_helper.rb | 11 +++-------- app/models/project.rb | 12 ++++++++++++ app/models/project_services/issue_tracker_service.rb | 4 ++++ app/models/project_services/redmine_service.rb | 4 ++-- lib/gitlab/markdown.rb | 7 +++---- 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index bcf108c5c4..b6ca2a057f 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -16,7 +16,7 @@ module IssuesHelper def url_for_project_issues(project = @project) return '' if project.nil? - if project.used_default_issues_tracker? || !external_issues_tracker_enabled? + if project.used_default_issues_tracker? || !project.external_issues_tracker_enabled? project_issues_path(project) else url = Gitlab.config.issues_tracker[project.issues_tracker]['project_url'] @@ -28,7 +28,7 @@ module IssuesHelper def url_for_new_issue(project = @project) return '' if project.nil? - if project.used_default_issues_tracker? || !external_issues_tracker_enabled? + if project.used_default_issues_tracker? || !project.external_issues_tracker_enabled? url = new_project_issue_path project_id: project else issues_tracker = Gitlab.config.issues_tracker[project.issues_tracker] @@ -41,7 +41,7 @@ module IssuesHelper def url_for_issue(issue_iid, project = @project) return '' if project.nil? - if project.used_default_issues_tracker? || !external_issues_tracker_enabled? + if project.used_default_issues_tracker? || !project.external_issues_tracker_enabled? url = project_issue_url project_id: project, id: issue_iid else url = Gitlab.config.issues_tracker[project.issues_tracker]['issues_url'] @@ -77,11 +77,6 @@ module IssuesHelper ts.html_safe end - # Checks if issues_tracker setting exists in gitlab.yml - def external_issues_tracker_enabled? - Gitlab.config.issues_tracker && Gitlab.config.issues_tracker.values.any? - end - def bulk_update_milestone_options options_for_select(['None (backlog)']) + options_from_collection_for_select(project_active_milestones, 'id', diff --git a/app/models/project.rb b/app/models/project.rb index e501ccb59f..0fff514997 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -309,6 +309,18 @@ class Project < ActiveRecord::Base self.issues_tracker == Project.issues_tracker.default_value end + def external_issues_tracker_enabled? + external_issues_trackers.any? + end + + def external_issues_trackers + services.select { |service| service.category == :issue_tracker } + end + + def external_issue_tracker + @external_issues_tracker ||= external_issues_trackers.select(&:activated?).first + end + def can_have_issues_tracker_id? self.issues_enabled && !self.used_default_issues_tracker? end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 4ba2f5a9ca..664b55a595 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -1,5 +1,9 @@ class IssueTrackerService < Service + def category + :issue_tracker + end + def project_url # implement inside child end diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index 03f7115d84..71286d74b5 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -48,9 +48,9 @@ class RedmineService < IssueTrackerService end def set_project_url - id = self.project.issue_tracker_id + id = self.project.issues_tracker_id if id - issues_tracker['project_url'].gsub(":issue_tracker_id", id) + issues_tracker['project_url'].gsub(":issues_tracker_id", id) else issues_tracker['project_url'] end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 068c342398..5987ee8da9 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -208,7 +208,7 @@ module Gitlab end def reference_issue(identifier, project = @project, prefix_text = nil) - if project.used_default_issues_tracker? || !external_issues_tracker_enabled? + if project.used_default_issues_tracker? || !project.external_issues_tracker_enabled? if project.issue_exists? identifier url = url_for_issue(identifier, project) title = title_for_issue(identifier, project) @@ -220,8 +220,7 @@ module Gitlab link_to("#{prefix_text}##{identifier}", url, options) end else - config = Gitlab.config - external_issue_tracker = config.issues_tracker[project.issues_tracker] + external_issue_tracker = project.external_issue_tracker if external_issue_tracker.present? reference_external_issue(identifier, external_issue_tracker, project, prefix_text) @@ -270,7 +269,7 @@ module Gitlab def reference_external_issue(identifier, issue_tracker, project = @project, prefix_text = nil) url = url_for_issue(identifier, project) - title = issue_tracker['title'] + title = issue_tracker.title options = html_options.merge( title: "Issue in #{title}", From 99736d45946c3c81f5c0e266722fece1272b7544 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Jan 2015 10:39:23 -0800 Subject: [PATCH 041/124] Make all avatars rounded for consistency --- app/assets/stylesheets/generic/avatar.scss | 5 +++-- app/assets/stylesheets/generic/timeline.scss | 2 +- app/assets/stylesheets/main/variables.scss | 2 ++ app/assets/stylesheets/sections/header.scss | 2 +- app/assets/stylesheets/sections/profile.scss | 9 --------- app/views/users/_groups.html.haml | 7 ++++--- 6 files changed, 11 insertions(+), 16 deletions(-) diff --git a/app/assets/stylesheets/generic/avatar.scss b/app/assets/stylesheets/generic/avatar.scss index 4f038b977e..4e0e546872 100644 --- a/app/assets/stylesheets/generic/avatar.scss +++ b/app/assets/stylesheets/generic/avatar.scss @@ -2,8 +2,9 @@ float: left; margin-right: 12px; width: 40px; - padding: 1px; - @include border-radius(4px); + height: 40px; + padding: 0; + @include border-radius($avatar_radius); &.avatar-inline { float: none; diff --git a/app/assets/stylesheets/generic/timeline.scss b/app/assets/stylesheets/generic/timeline.scss index ee3bf9c1b2..f92a79f7a5 100644 --- a/app/assets/stylesheets/generic/timeline.scss +++ b/app/assets/stylesheets/generic/timeline.scss @@ -42,7 +42,7 @@ background: #fff; color: #737881; float: left; - @include border-radius(40px); + @include border-radius($avatar_radius); @include box-shadow(0 0 0 3px #EEE); overflow: hidden; diff --git a/app/assets/stylesheets/main/variables.scss b/app/assets/stylesheets/main/variables.scss index 32fde32c42..6bbce70a78 100644 --- a/app/assets/stylesheets/main/variables.scss +++ b/app/assets/stylesheets/main/variables.scss @@ -57,3 +57,5 @@ $list-font-size: 15px; * Sidebar navigation width */ $sidebar_width: 230px; + +$avatar_radius: 50%; diff --git a/app/assets/stylesheets/sections/header.scss b/app/assets/stylesheets/sections/header.scss index a5098b6da5..047617e54b 100644 --- a/app/assets/stylesheets/sections/header.scss +++ b/app/assets/stylesheets/sections/header.scss @@ -140,7 +140,7 @@ header { img { width: 26px; height: 26px; - @include border-radius(4px); + @include border-radius($avatar_radius); } } diff --git a/app/assets/stylesheets/sections/profile.scss b/app/assets/stylesheets/sections/profile.scss index 086875582f..0ab62b7ae4 100644 --- a/app/assets/stylesheets/sections/profile.scss +++ b/app/assets/stylesheets/sections/profile.scss @@ -102,12 +102,3 @@ } } } - -.profile-groups-avatars { - margin: 0 5px 10px 0; - - img { - width: 50px; - height: 50px; - } -} diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml index ea008c2ded..b9bd6aa376 100644 --- a/app/views/users/_groups.html.haml +++ b/app/views/users/_groups.html.haml @@ -1,3 +1,4 @@ -- groups.each do |group| - = link_to group, class: 'profile-groups-avatars', :title => group.name do - - image_tag group_icon(group.path) +.clearfix + - groups.each do |group| + = link_to group, class: 'profile-groups-avatars', title: group.name do + = image_tag group_icon(group.path), class: 'avatar s40' From 31bf578d67620dfc904ff2f980788fe38ee9ca92 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Fri, 23 Jan 2015 10:55:12 -0800 Subject: [PATCH 042/124] Increase password reset timeout since other people trigger it when they create an account for you. --- CHANGELOG | 2 +- config/initializers/devise.rb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bd51900253..63b70f8bc7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -37,7 +37,7 @@ v 7.8.0 - - - - - + - Password reset token validity increased from 2 hours to 2 days since it is also send on account creation. - - - diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index c6eb3e5103..79abe3c695 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -145,7 +145,8 @@ Devise.setup do |config| # Time interval you can reset your password with a reset password key. # Don't put a too small interval or your users won't have the time to # change their passwords. - config.reset_password_within = 2.hours + # When someone else invites you to GitLab this time is also used so it should be pretty long. + config.reset_password_within = 2.days # ==> Configuration for :encryptable # Allow you to use another encryption algorithm besides bcrypt (default). You can use From 7c701acf573f95cce7f8b1b7756de5d73404f09b Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 23 Jan 2015 11:01:09 -0800 Subject: [PATCH 043/124] Do a check which issue tracker is used inside the project. --- app/helpers/issues_helper.rb | 8 ++++---- app/models/project.rb | 10 +++++++--- app/views/layouts/nav/_project.html.haml | 2 +- lib/gitlab/markdown.rb | 2 +- spec/models/project_spec.rb | 6 +++--- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index b6ca2a057f..d3bb1d3920 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -16,7 +16,7 @@ module IssuesHelper def url_for_project_issues(project = @project) return '' if project.nil? - if project.used_default_issues_tracker? || !project.external_issues_tracker_enabled? + if project.using_issue_tracker? project_issues_path(project) else url = Gitlab.config.issues_tracker[project.issues_tracker]['project_url'] @@ -28,7 +28,7 @@ module IssuesHelper def url_for_new_issue(project = @project) return '' if project.nil? - if project.used_default_issues_tracker? || !project.external_issues_tracker_enabled? + if project.using_issue_tracker? url = new_project_issue_path project_id: project else issues_tracker = Gitlab.config.issues_tracker[project.issues_tracker] @@ -41,7 +41,7 @@ module IssuesHelper def url_for_issue(issue_iid, project = @project) return '' if project.nil? - if project.used_default_issues_tracker? || !project.external_issues_tracker_enabled? + if project.using_issue_tracker? url = project_issue_url project_id: project, id: issue_iid else url = Gitlab.config.issues_tracker[project.issues_tracker]['issues_url'] @@ -54,7 +54,7 @@ module IssuesHelper def title_for_issue(issue_iid, project = @project) return '' if project.nil? - if project.used_default_issues_tracker? + if project.default_issues_tracker? issue = project.issues.where(iid: issue_iid).first return issue.title if issue end diff --git a/app/models/project.rb b/app/models/project.rb index 0fff514997..a79e74105b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -298,14 +298,14 @@ class Project < ActiveRecord::Base end def issue_exists?(issue_id) - if used_default_issues_tracker? + if default_issues_tracker? self.issues.where(iid: issue_id).first.present? else true end end - def used_default_issues_tracker? + def default_issues_tracker? self.issues_tracker == Project.issues_tracker.default_value end @@ -321,8 +321,12 @@ class Project < ActiveRecord::Base @external_issues_tracker ||= external_issues_trackers.select(&:activated?).first end + def using_issue_tracker? + default_issues_tracker? || !external_issues_tracker_enabled? + end + def can_have_issues_tracker_id? - self.issues_enabled && !self.used_default_issues_tracker? + self.issues_enabled && !self.default_issues_tracker? end def build_missing_services diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 94cee0bd50..07ac4be204 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -39,7 +39,7 @@ %i.fa.fa-exclamation-circle %span Issues - - if @project.used_default_issues_tracker? + - if @project.default_issues_tracker? %span.count.issue_counter= @project.issues.opened.count - if project_nav_tab? :merge_requests diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 5987ee8da9..6ba7a0c18f 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -208,7 +208,7 @@ module Gitlab end def reference_issue(identifier, project = @project, prefix_text = nil) - if project.used_default_issues_tracker? || !project.external_issues_tracker_enabled? + if project.using_issue_tracker? if project.issue_exists? identifier url = url_for_issue(identifier, project) title = title_for_issue(identifier, project) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 70a15cac1a..9d633e7818 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -195,16 +195,16 @@ describe Project do end end - describe :used_default_issues_tracker? do + describe :default_issues_tracker? do let(:project) { create(:project) } let(:ext_project) { create(:redmine_project) } it "should be true if used internal tracker" do - project.used_default_issues_tracker?.should be_true + project.default_issues_tracker?.should be_true end it "should be false if used other tracker" do - ext_project.used_default_issues_tracker?.should be_false + ext_project.default_issues_tracker?.should be_false end end From 041bad0fe17a31eb7becde3a656a1d0e50dc85bf Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 23 Jan 2015 11:10:06 -0800 Subject: [PATCH 044/124] Manipulate external tracker issues urls generated from services. --- app/helpers/issues_helper.rb | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index d3bb1d3920..cfbbed842c 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -19,9 +19,7 @@ module IssuesHelper if project.using_issue_tracker? project_issues_path(project) else - url = Gitlab.config.issues_tracker[project.issues_tracker]['project_url'] - url.gsub(':project_id', project.id.to_s). - gsub(':issues_tracker_id', project.issues_tracker_id.to_s) + project.external_issue_tracker.project_url end end @@ -31,10 +29,7 @@ module IssuesHelper if project.using_issue_tracker? url = new_project_issue_path project_id: project else - issues_tracker = Gitlab.config.issues_tracker[project.issues_tracker] - url = issues_tracker['new_issue_url'] - url.gsub(':project_id', project.id.to_s). - gsub(':issues_tracker_id', project.issues_tracker_id.to_s) + project.external_issue_tracker.new_issue_url end end @@ -44,10 +39,8 @@ module IssuesHelper if project.using_issue_tracker? url = project_issue_url project_id: project, id: issue_iid else - url = Gitlab.config.issues_tracker[project.issues_tracker]['issues_url'] - url.gsub(':id', issue_iid.to_s). - gsub(':project_id', project.id.to_s). - gsub(':issues_tracker_id', project.issues_tracker_id.to_s) + url = project.external_issue_tracker.issues_url + url.gsub(':id', issue_iid.to_s) end end From 061ac7923670083cf7237a32867b2f8ce86a1c83 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Fri, 23 Jan 2015 11:18:10 -0800 Subject: [PATCH 045/124] Rename workflow to feature branch flow and add better desciption of workflow category. --- doc/README.md | 2 +- doc/workflow/README.md | 2 +- doc/workflow/workflow.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/README.md b/doc/README.md index 3c8f8ad3d0..8c6d13e850 100644 --- a/doc/README.md +++ b/doc/README.md @@ -9,7 +9,7 @@ - [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [Web hooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. -- [Workflow](workflow/README.md) Learn how to get the maximum out of GitLab. +- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. ## Administrator documentation diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 1fe63274c2..33176aaba4 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -1,6 +1,6 @@ # Workflow -- [Workflow](workflow.md) +- [Feature branch workflow](workflow.md) - [Project Features](project_features.md) - [Authorization for merge requests](authorization_for_merge_requests.md) - [Groups](groups.md) diff --git a/doc/workflow/workflow.md b/doc/workflow/workflow.md index ab29cfb670..f70e41df84 100644 --- a/doc/workflow/workflow.md +++ b/doc/workflow/workflow.md @@ -1,4 +1,4 @@ -# Workflow +# Feature branch workflow 1. Clone project: From a720dde67c2b488117ed57f7f07ab55f8150c352 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Fri, 23 Jan 2015 11:55:41 -0800 Subject: [PATCH 046/124] Remove configuration option from project settings page for external issue trackers. --- app/helpers/projects_helper.rb | 12 ------------ app/models/project.rb | 2 +- app/models/service.rb | 8 ++++++++ app/views/projects/edit.html.haml | 9 --------- 4 files changed, 9 insertions(+), 22 deletions(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 9a31d48518..fea78c6e38 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -72,18 +72,6 @@ module ProjectsHelper @project.milestones.active.order("due_date, title ASC") end - def project_issues_trackers(current_tracker = nil) - values = Project.issues_tracker.values.map do |tracker_key| - if tracker_key.to_sym == :gitlab - ['GitLab', tracker_key] - else - [Gitlab.config.issues_tracker[tracker_key]['title'] || tracker_key, tracker_key] - end - end - - options_for_select(values, current_tracker) - end - def link_to_toggle_star(title, starred, signed_in) cls = 'star-btn' cls += ' disabled' unless signed_in diff --git a/app/models/project.rb b/app/models/project.rb index a79e74105b..4bf36255a5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -137,7 +137,7 @@ class Project < ActiveRecord::Base scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) } scope :non_archived, -> { where(archived: false) } - enumerize :issues_tracker, in: (Gitlab.config.issues_tracker.keys).append(:gitlab), default: :gitlab + enumerize :issues_tracker, in: (Service.issue_tracker_service_list).append(:gitlab), default: :gitlab state_machine :import_status, initial: :none do event :import_start do diff --git a/app/models/service.rb b/app/models/service.rb index 71c8aa39e4..4241947534 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -86,4 +86,12 @@ class Service < ActiveRecord::Base def async_execute(data) Sidekiq::Client.enqueue(ProjectServiceWorker, id, data) end + + def issue_tracker? + self.category == :issue_tracker + end + + def self.issue_tracker_service_list + Service.select(&:issue_tracker?).map{ |s| s.to_param } + end end diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index f2bb56b566..fb4d827073 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -50,15 +50,6 @@ = f.check_box :issues_enabled %span.descr Lightweight issue tracking system for this project - - if Project.issues_tracker.values.count > 1 - .form-group - = f.label :issues_tracker, "Issues tracker", class: 'control-label' - .col-sm-10= f.select(:issues_tracker, project_issues_trackers(@project.issues_tracker), {}, { disabled: !@project.issues_enabled }) - - .form-group - = f.label :issues_tracker_id, "Project name or id in issues tracker", class: 'control-label' - .col-sm-10= f.text_field :issues_tracker_id, disabled: !@project.can_have_issues_tracker_id?, class: 'form-control' - .form-group = f.label :merge_requests_enabled, "Merge Requests", class: 'control-label' .col-sm-10 From a42d84eca30956ca9255f93e6803efb43e1874cf Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Jan 2015 14:38:20 -0800 Subject: [PATCH 047/124] Faster autocomplete without unused description --- app/controllers/projects_controller.rb | 2 +- app/services/projects/autocomplete_service.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 7fc283ef3d..ae9d942853 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -119,7 +119,7 @@ class ProjectsController < ApplicationController } respond_to do |format| - format.json { render :json => @suggestions } + format.json { render json: @suggestions } end end diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 09fc25cc1b..7408e09ed1 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -5,11 +5,11 @@ module Projects end def issues - @project.issues.opened.select([:iid, :title, :description]) + @project.issues.opened.select([:iid, :title]) end def merge_requests - @project.merge_requests.opened.select([:iid, :title, :description]) + @project.merge_requests.opened.select([:iid, :title]) end end end From 893a68baf34bce26cb7cab0bc486b0b791308176 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Jan 2015 16:17:27 -0800 Subject: [PATCH 048/124] Cache autocomplete or emojis --- app/controllers/projects_controller.rb | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ae9d942853..89296b9aa4 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -104,15 +104,8 @@ class ProjectsController < ApplicationController autocomplete = ::Projects::AutocompleteService.new(@project) participants = ::Projects::ParticipantsService.new(@project).execute(note_type, note_id) - emojis = Emoji.names.map do |e| - { - name: e, - path: view_context.image_url("emoji/#{e}.png") - } - end - @suggestions = { - emojis: emojis, + emojis: autocomplete_emojis, issues: autocomplete.issues, mergerequests: autocomplete.merge_requests, members: participants @@ -189,4 +182,15 @@ class ProjectsController < ApplicationController :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id ) end + + def autocomplete_emojis + Rails.cache.fetch("autocomplete-emoji-#{Emoji::VERSION}") do + Emoji.names.map do |e| + { + name: e, + path: view_context.image_url("emoji/#{e}.png") + } + end + end + end end From 3dfcb95f0d5a9851b3829f357bc53abb96c0e6ba Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Jan 2015 17:41:10 -0800 Subject: [PATCH 049/124] Use ruby 1.9 hash syntax --- app/controllers/passwords_controller.rb | 4 ++-- app/controllers/projects/protected_branches_controller.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 988ede3007..dcbbe5baa4 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -5,12 +5,12 @@ class PasswordsController < Devise::PasswordsController resource_found = resource_class.find_by_email(email) if resource_found && resource_found.ldap_user? flash[:alert] = "Cannot reset password for LDAP user." - respond_with({}, :location => after_sending_reset_password_instructions_path_for(resource_name)) and return + respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name)) and return end self.resource = resource_class.send_reset_password_instructions(resource_params) if successfully_sent?(resource) - respond_with({}, :location => after_sending_reset_password_instructions_path_for(resource_name)) + respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name)) else respond_with(resource) end diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index 02160d973b..f45df38b87 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -24,7 +24,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController ) respond_to do |format| - format.json { render :json => protected_branch, status: :ok } + format.json { render json: protected_branch, status: :ok } end else respond_to do |format| From 0b404c3599841fba3c118ecd6eb996b074fcfa11 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Jan 2015 18:14:26 -0800 Subject: [PATCH 050/124] Show no-ssh error message for project page --- app/views/projects/empty.html.haml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 59f19c8b7a..2e46de6bfe 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,3 +1,6 @@ +- if current_user && can?(current_user, :download_code, @project) + = render 'shared/no_ssh' + = render "home_panel" %div.git-empty From a250fa5feb310ca6def85ae3b9947247ac2ecf98 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Jan 2015 18:17:55 -0800 Subject: [PATCH 051/124] Make new project button more visible --- app/assets/stylesheets/sections/dashboard.scss | 9 +++++++++ app/views/dashboard/_projects.html.haml | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/sections/dashboard.scss b/app/assets/stylesheets/sections/dashboard.scss index e540f7ff94..824f136d30 100644 --- a/app/assets/stylesheets/sections/dashboard.scss +++ b/app/assets/stylesheets/sections/dashboard.scss @@ -97,3 +97,12 @@ margin-right: 3px; width: 16px; } + +.dash-new-project { + background: $bg_success; + border: 1px solid $border_success; + + a { + color: #FFF; + } +} diff --git a/app/views/dashboard/_projects.html.haml b/app/views/dashboard/_projects.html.haml index 304aa17eba..0596738342 100644 --- a/app/views/dashboard/_projects.html.haml +++ b/app/views/dashboard/_projects.html.haml @@ -3,8 +3,8 @@ .input-group = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'dash-filter form-control' - if current_user.can_create_project? - .input-group-addon - = link_to new_project_path, class: "" do + .input-group-addon.dash-new-project + = link_to new_project_path do %strong New project %ul.well-list.dash-list From 1b1277af46d894a0ed8b9ed625a0fe361863970c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Jan 2015 18:18:04 -0800 Subject: [PATCH 052/124] Fix avatar indentation --- app/assets/stylesheets/generic/avatar.scss | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/generic/avatar.scss b/app/assets/stylesheets/generic/avatar.scss index 4e0e546872..8051461555 100644 --- a/app/assets/stylesheets/generic/avatar.scss +++ b/app/assets/stylesheets/generic/avatar.scss @@ -8,10 +8,11 @@ &.avatar-inline { float: none; - margin-left: 3px; + margin-left: 4px; + margin-bottom: 2px; - &.s16 { margin-right: 2px; } - &.s24 { margin-right: 2px; } + &.s16 { margin-right: 4px; } + &.s24 { margin-right: 4px; } } &.s16 { width: 16px; height: 16px; margin-right: 6px; } From d3d64a4512566015d4ee523cddef4f393c194b45 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Jan 2015 20:41:51 -0800 Subject: [PATCH 053/124] Fix tooltip for groups on user page --- app/views/users/_groups.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml index b9bd6aa376..32a1dc83b5 100644 --- a/app/views/users/_groups.html.haml +++ b/app/views/users/_groups.html.haml @@ -1,4 +1,4 @@ .clearfix - groups.each do |group| = link_to group, class: 'profile-groups-avatars', title: group.name do - = image_tag group_icon(group.path), class: 'avatar s40' + = image_tag group_icon(group.path), class: 'avatar avatar-inline s40' From a6bdf7d8769c1726084f42a510b6aed097fffbf2 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Jan 2015 20:50:17 -0800 Subject: [PATCH 054/124] Show success/error message for test services button --- CHANGELOG | 2 +- app/controllers/projects/services_controller.rb | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 63b70f8bc7..06a10e379f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -22,7 +22,7 @@ v 7.8.0 - - Upgrade Sidekiq gem to version 3.3.0 - Stop git zombie creation during force push check - - + - Show success/error messages for test setting button in services - - Fix commits pagination - diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index b2ce99aeb4..5ac6947c5d 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -25,9 +25,13 @@ class Projects::ServicesController < Projects::ApplicationController def test data = Gitlab::PushDataBuilder.build_sample(project, current_user) - @service.execute(data) + if @service.execute(data) + message = { notice: 'We sent a request to the provided URL' } + else + message = { alert: 'We tried to send a request to the provided URL but error occured' } + end - redirect_to :back + redirect_to :back, message end private From 5e98293b0e21a7fd9d62ce7272afef31b8414abb Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Fri, 23 Jan 2015 21:03:12 -0800 Subject: [PATCH 055/124] Redesign services page --- app/views/projects/services/index.html.haml | 25 ++++++++++++++------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/index.html.haml index 7271dd830c..4604c0afd8 100644 --- a/app/views/projects/services/index.html.haml +++ b/app/views/projects/services/index.html.haml @@ -1,13 +1,22 @@ %h3.page-title Project services %p.light Project services allow you to integrate GitLab with other applications -%hr -%ul.bordered-list +%table.table + %thead + %tr + %th + %th Service + %th Desription + %th Last edit - @services.sort_by(&:title).each do |service| - %li - %h4 + %tr + %td + = boolean_to_icon service.activated? + %td = link_to edit_project_service_path(@project, service.to_param) do - = service.title - .pull-right - = boolean_to_icon service.activated? - %p= service.description + %strong= service.title + %td + = service.description + %td.light + = time_ago_in_words service.updated_at + ago From 7ac648911fb018dafa2708fe29ed59847fe875a1 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Sat, 24 Jan 2015 16:40:25 +0200 Subject: [PATCH 056/124] Add titles to links. Closes #1018. --- app/views/groups/_settings_nav.html.haml | 4 ++-- app/views/layouts/nav/_admin.html.haml | 18 +++++++++--------- app/views/layouts/nav/_dashboard.html.haml | 8 ++++---- app/views/layouts/nav/_group.html.haml | 10 +++++----- app/views/layouts/nav/_profile.html.haml | 19 +++++++++---------- app/views/layouts/nav/_project.html.haml | 19 +++++++++---------- app/views/projects/_settings_nav.html.haml | 12 ++++++------ 7 files changed, 44 insertions(+), 46 deletions(-) diff --git a/app/views/groups/_settings_nav.html.haml b/app/views/groups/_settings_nav.html.haml index 35180792a0..e6aee22e52 100644 --- a/app/views/groups/_settings_nav.html.haml +++ b/app/views/groups/_settings_nav.html.haml @@ -1,11 +1,11 @@ %ul.sidebar-subnav = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group) do + = link_to edit_group_path(@group), title: 'Group' do %i.fa.fa-pencil-square-o %span Group = nav_link(path: 'groups#projects') do - = link_to projects_group_path(@group) do + = link_to projects_group_path(@group), title: 'Projects' do %i.fa.fa-folder %span Projects diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index d9c6670d1b..66770adb5a 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -5,49 +5,49 @@ %span Overview = nav_link(controller: :projects) do - = link_to admin_projects_path do + = link_to admin_projects_path, title: 'Projects' do %i.fa.fa-cube %span Projects = nav_link(controller: :users) do - = link_to admin_users_path do + = link_to admin_users_path, title: 'Users' do %i.fa.fa-user %span Users = nav_link(controller: :groups) do - = link_to admin_groups_path do + = link_to admin_groups_path, title: 'Groups' do %i.fa.fa-group %span Groups = nav_link(controller: :logs) do - = link_to admin_logs_path do + = link_to admin_logs_path, title: 'Logs' do %i.fa.fa-file-text %span Logs = nav_link(controller: :broadcast_messages) do - = link_to admin_broadcast_messages_path do + = link_to admin_broadcast_messages_path, title: 'Broadcast Messages' do %i.fa.fa-bullhorn %span Messages = nav_link(controller: :hooks) do - = link_to admin_hooks_path do + = link_to admin_hooks_path, title: 'Hooks' do %i.fa.fa-external-link %span Hooks = nav_link(controller: :background_jobs) do - = link_to admin_background_jobs_path do + = link_to admin_background_jobs_path, title: 'Background Jobs' do %i.fa.fa-cog %span Background Jobs = nav_link(controller: :application_settings) do - = link_to admin_application_settings_path do + = link_to admin_application_settings_path, title: 'Settings' do %i.fa.fa-cogs %span Settings = nav_link(controller: :applications) do - = link_to admin_applications_path do + = link_to admin_applications_path, title: 'Applications' do %i.fa.fa-cloud %span Applications diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index a2eaa2d83c..48c7c99942 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -5,24 +5,24 @@ %span Activity = nav_link(path: 'dashboard#projects') do - = link_to projects_dashboard_path, class: 'shortcuts-projects' do + = link_to projects_dashboard_path, title: 'Projects', class: 'shortcuts-projects' do %i.fa.fa-cube %span Projects = nav_link(path: 'dashboard#issues') do - = link_to assigned_issues_dashboard_path, class: 'shortcuts-issues' do + = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues' do %i.fa.fa-exclamation-circle %span Issues %span.count= current_user.assigned_issues.opened.count = nav_link(path: 'dashboard#merge_requests') do - = link_to assigned_mrs_dashboard_path, class: 'shortcuts-merge_requests' do + = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do %i.fa.fa-tasks %span Merge Requests %span.count= current_user.assigned_merge_requests.opened.count = nav_link(controller: :help) do - = link_to help_path do + = link_to help_path, title: 'Help' do %i.fa.fa-question-circle %span Help diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 54468d077a..ddd3df19ee 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -6,33 +6,33 @@ Activity - if current_user = nav_link(controller: [:group, :milestones]) do - = link_to group_milestones_path(@group) do + = link_to group_milestones_path(@group), title: 'Milestones' do %i.fa.fa-clock-o %span Milestones = nav_link(path: 'groups#issues') do - = link_to issues_group_path(@group) do + = link_to issues_group_path(@group), title: 'Issues' do %i.fa.fa-exclamation-circle %span Issues - if current_user %span.count= Issue.opened.of_group(@group).count = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group) do + = link_to merge_requests_group_path(@group), title: 'Merge Requests' do %i.fa.fa-tasks %span Merge Requests - if current_user %span.count= MergeRequest.opened.of_group(@group).count = nav_link(path: 'groups#members') do - = link_to members_group_path(@group) do + = link_to members_group_path(@group), title: 'Members' do %i.fa.fa-users %span Members - if can?(current_user, :manage_group, @group) = nav_link(html_options: { class: "#{"active" if group_settings_page?} separate-item" }) do - = link_to edit_group_path(@group), class: "tab no-highlight" do + = link_to edit_group_path(@group), title: 'Settings', class: "tab no-highlight" do %i.fa.fa-cogs %span Settings diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index cc50b9b570..0914d2a167 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -5,52 +5,51 @@ %span Profile = nav_link(controller: :accounts) do - = link_to profile_account_path do + = link_to profile_account_path, title: 'Account' do %i.fa.fa-gear %span Account = nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new']) do - = link_to applications_profile_path do + = link_to applications_profile_path, title: 'Applications' do %i.fa.fa-cloud %span Applications = nav_link(controller: :emails) do - = link_to profile_emails_path do + = link_to profile_emails_path, title: 'Emails' do %i.fa.fa-envelope-o %span Emails %span.count= current_user.emails.count + 1 - unless current_user.ldap_user? = nav_link(controller: :passwords) do - = link_to edit_profile_password_path do + = link_to edit_profile_password_path, title: 'Password' do %i.fa.fa-lock %span Password = nav_link(controller: :notifications) do - = link_to profile_notifications_path do + = link_to profile_notifications_path, title: 'Notifications' do %i.fa.fa-inbox %span Notifications = nav_link(controller: :keys) do - = link_to profile_keys_path do + = link_to profile_keys_path, title: 'SSH Keys' do %i.fa.fa-key %span SSH Keys %span.count= current_user.keys.count = nav_link(path: 'profiles#design') do - = link_to design_profile_path do + = link_to design_profile_path, title: 'Design' do %i.fa.fa-image %span Design = nav_link(controller: :groups) do - = link_to profile_groups_path do + = link_to profile_groups_path, title: 'Groups' do %i.fa.fa-group %span Groups = nav_link(path: 'profiles#history') do - = link_to history_profile_path do + = link_to history_profile_path, title: 'History' do %i.fa.fa-history %span History - diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 94cee0bd50..502e350300 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -6,36 +6,35 @@ Project - if project_nav_tab? :files = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do - = link_to project_tree_path(@project, @ref || @repository.root_ref), class: 'shortcuts-tree' do + = link_to project_tree_path(@project, @ref || @repository.root_ref), title: 'Files', class: 'shortcuts-tree' do %i.fa.fa-files-o %span Files - - if project_nav_tab? :commits = nav_link(controller: %w(commit commits compare repositories tags branches)) do - = link_to project_commits_path(@project, @ref || @repository.root_ref), class: 'shortcuts-commits' do + = link_to project_commits_path(@project, @ref || @repository.root_ref), title: 'Commits', class: 'shortcuts-commits' do %i.fa.fa-history %span Commits - if project_nav_tab? :network = nav_link(controller: %w(network)) do - = link_to project_network_path(@project, @ref || @repository.root_ref), class: 'shortcuts-network' do + = link_to project_network_path(@project, @ref || @repository.root_ref), title: 'Network', class: 'shortcuts-network' do %i.fa.fa-code-fork %span Network - if project_nav_tab? :graphs = nav_link(controller: %w(graphs)) do - = link_to project_graph_path(@project, @ref || @repository.root_ref), class: 'shortcuts-graphs' do + = link_to project_graph_path(@project, @ref || @repository.root_ref), title: 'Graphs', class: 'shortcuts-graphs' do %i.fa.fa-area-chart %span Graphs - if project_nav_tab? :issues = nav_link(controller: %w(issues milestones labels)) do - = link_to url_for_project_issues, class: 'shortcuts-issues' do + = link_to url_for_project_issues, title: 'Issues', class: 'shortcuts-issues' do %i.fa.fa-exclamation-circle %span Issues @@ -44,7 +43,7 @@ - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do - = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests' do + = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %i.fa.fa-tasks %span Merge Requests @@ -52,21 +51,21 @@ - if project_nav_tab? :wiki = nav_link(controller: :wikis) do - = link_to project_wiki_path(@project, :home), class: 'shortcuts-wiki' do + = link_to project_wiki_path(@project, :home), title: 'Wiki', class: 'shortcuts-wiki' do %i.fa.fa-book %span Wiki - if project_nav_tab? :snippets = nav_link(controller: :snippets) do - = link_to project_snippets_path(@project), class: 'shortcuts-snippets' do + = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do %i.fa.fa-file-text-o %span Snippets - if project_nav_tab? :settings = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do - = link_to edit_project_path(@project), class: "stat-tab tab no-highlight" do + = link_to edit_project_path(@project), title: 'Settings', class: "stat-tab tab no-highlight" do %i.fa.fa-cogs %span Settings diff --git a/app/views/projects/_settings_nav.html.haml b/app/views/projects/_settings_nav.html.haml index 64eda0bf28..646e48a1e1 100644 --- a/app/views/projects/_settings_nav.html.haml +++ b/app/views/projects/_settings_nav.html.haml @@ -1,31 +1,31 @@ %ul.project-settings-nav.sidebar-subnav = nav_link(path: 'projects#edit') do - = link_to edit_project_path(@project), class: "stat-tab tab " do + = link_to edit_project_path(@project), title: 'Project', class: "stat-tab tab " do %i.fa.fa-pencil-square-o %span Project = nav_link(controller: [:team_members, :teams]) do - = link_to project_team_index_path(@project), class: "team-tab tab" do + = link_to project_team_index_path(@project), title: 'Members', class: "team-tab tab" do %i.fa.fa-users %span Members = nav_link(controller: :deploy_keys) do - = link_to project_deploy_keys_path(@project) do + = link_to project_deploy_keys_path(@project), title: 'Deploy Keys' do %i.fa.fa-key %span Deploy Keys = nav_link(controller: :hooks) do - = link_to project_hooks_path(@project) do + = link_to project_hooks_path(@project), title: 'Web Hooks' do %i.fa.fa-link %span Web Hooks = nav_link(controller: :services) do - = link_to project_services_path(@project) do + = link_to project_services_path(@project), title: 'Services' do %i.fa.fa-cogs %span Services = nav_link(controller: :protected_branches) do - = link_to project_protected_branches_path(@project) do + = link_to project_protected_branches_path(@project), title: 'Protected Branches' do %i.fa.fa-lock %span Protected branches From 42bac7f9f27b0e8fb113e452fc2106882262172d Mon Sep 17 00:00:00 2001 From: Steven Thonus Date: Sat, 25 Jan 2014 18:15:44 +0100 Subject: [PATCH 057/124] adding avatar to project settings page added avatar removal show project avatar on dashboard, projects page, project page added rspec and feature tests added project avatar from repository new default project icon added added copying af avatar to forking of project added generated icon fixed avatar fork hound fix style fix test fix --- app/assets/images/no_project_icon.png | Bin 0 -> 3387 bytes app/assets/javascripts/project.js.coffee | 10 ++++ app/assets/stylesheets/generic/avatar.scss | 13 +++++ .../stylesheets/sections/dashboard.scss | 6 +++ .../projects/avatars_controller.rb | 29 +++++++++++ app/helpers/application_helper.rb | 25 ++++++++++ app/models/project.rb | 25 ++++++++++ app/services/projects/fork_service.rb | 3 ++ app/views/dashboard/_project.html.haml | 2 + app/views/dashboard/projects.html.haml | 2 + app/views/projects/_home_panel.html.haml | 1 + app/views/projects/edit.html.haml | 28 ++++++++++- config/routes.rb | 2 + .../20140125162722_add_avatar_to_projects.rb | 5 ++ db/schema.rb | 1 + features/project/project.feature | 13 +++++ features/steps/project/project.rb | 47 ++++++++++++++++-- spec/helpers/application_helper_spec.rb | 22 ++++++++ spec/models/project_spec.rb | 15 ++++++ spec/routing/project_routing_spec.rb | 7 +++ 20 files changed, 252 insertions(+), 4 deletions(-) create mode 100644 app/assets/images/no_project_icon.png create mode 100644 app/controllers/projects/avatars_controller.rb create mode 100644 db/migrate/20140125162722_add_avatar_to_projects.rb diff --git a/app/assets/images/no_project_icon.png b/app/assets/images/no_project_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8e9529c67ec3a7167c59f2c52bdb4b009b9803cf GIT binary patch literal 3387 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 + form = $(this).closest("form") + form.find(".js-project-avatar-input").click() + + $('.js-project-avatar-input').bind "change", -> + form = $(this).closest("form") + filename = $(this).val().replace(/^.*[\\\/]/, '') + form.find(".js-avatar-filename").text(filename) diff --git a/app/assets/stylesheets/generic/avatar.scss b/app/assets/stylesheets/generic/avatar.scss index 8051461555..f04848ae6d 100644 --- a/app/assets/stylesheets/generic/avatar.scss +++ b/app/assets/stylesheets/generic/avatar.scss @@ -23,3 +23,16 @@ &.s90 { width: 90px; height: 90px; margin-right: 15px; } &.s160 { width: 160px; height: 160px; margin-right: 20px; } } + +.identicon { + text-align: center; + vertical-align: top; + + &.s16 { font-size: 12px; line-height: 1.33; } + &.s24 { font-size: 18px; line-height: 1.33; } + &.s26 { font-size: 20px; line-height: 1.33; } + &.s32 { font-size: 24px; line-height: 1.33; } + &.s60 { font-size: 45px; line-height: 1.33; } + &.s90 { font-size: 68px; line-height: 1.33; } + &.s160 { font-size: 120px; line-height: 1.33; } +} \ No newline at end of file diff --git a/app/assets/stylesheets/sections/dashboard.scss b/app/assets/stylesheets/sections/dashboard.scss index 824f136d30..3135056db5 100644 --- a/app/assets/stylesheets/sections/dashboard.scss +++ b/app/assets/stylesheets/sections/dashboard.scss @@ -75,6 +75,9 @@ } } } +.project-avatar { + float: left; +} .project-description { overflow: hidden; @@ -92,6 +95,9 @@ } } +.dash-project-avatar { + float: left; +} .dash-project-access-icon { float: left; margin-right: 3px; diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb new file mode 100644 index 0000000000..a482b90880 --- /dev/null +++ b/app/controllers/projects/avatars_controller.rb @@ -0,0 +1,29 @@ +class Projects::AvatarsController < Projects::ApplicationController + layout 'project' + + before_filter :project + + def show + @blob = @project.repository.blob_at_branch('master', @project.avatar_in_git) + if @blob + headers['X-Content-Type-Options'] = 'nosniff' + send_data( + @blob.data, + type: @blob.mime_type, + disposition: 'inline', + filename: @blob.name + ) + else + not_found! + end + end + + def destroy + @project.remove_avatar! + + @project.save + @project.reset_events_cache + + redirect_to edit_project_path(@project) + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f65e04af20..772400d55e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -50,6 +50,31 @@ module ApplicationHelper args.any? { |v| v.to_s.downcase == action_name } end + def project_icon(project_id, options = {}) + project = Project.find_with_namespace(project_id) + if project.avatar.present? + image_tag project.avatar.url, options + elsif options[:only_uploaded] + image_tag '/assets/no_project_icon.png', options + elsif project.avatar_in_git + image_tag project_avatar_path(project), options + else # generated icon + project_identicon(project, options) + end + end + + def project_identicon(project, options = {}) + options[:class] ||= '' + options[:class] << ' identicon' + bg_color = Digest::MD5.hexdigest(project.name)[0, 6] + brightness = bg_color[0, 2].hex + bg_color[2, 2].hex + bg_color[4, 2].hex + text_color = (brightness > 375) ? '#000' : '#fff' + content_tag(:div, class: options[:class], + style: "background-color: ##{ bg_color }; color: #{ text_color }") do + project.name[0, 1].upcase + end + end + def group_icon(group_path) group = Group.find_by(path: group_path) if group && group.avatar.present? diff --git a/app/models/project.rb b/app/models/project.rb index f102c47740..7160e704aa 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -26,6 +26,7 @@ # star_count :integer default(0), not null # import_type :string(255) # import_source :string(255) +# avatar :string(255) # class Project < ActiveRecord::Base @@ -119,6 +120,11 @@ class Project < ActiveRecord::Base if: :import? validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create + validate :avatar_type, + if: ->(project) { project.avatar && project.avatar_changed? } + validates :avatar, file_size: { maximum: 100.kilobytes.to_i } + + mount_uploader :avatar, AttachmentUploader # Scopes scope :without_user, ->(user) { where("projects.id NOT IN (:ids)", ids: user.authorized_projects.map(&:id) ) } @@ -338,6 +344,24 @@ class Project < ActiveRecord::Base @ci_service ||= ci_services.select(&:activated?).first end + def avatar_type + unless avatar.image? + errors.add :avatar, 'only images allowed' + end + end + + def avatar_in_git + @avatar_file ||= 'logo.png' if repository.blob_at_branch('master', 'logo.png') + @avatar_file ||= 'logo.jpg' if repository.blob_at_branch('master', 'logo.jpg') + @avatar_file ||= 'logo.gif' if repository.blob_at_branch('master', 'logo.gif') + @avatar_file + end + + # For compatibility with old code + def code + path + end + def items_for(entity) case entity when 'issue' then @@ -529,6 +553,7 @@ class Project < ActiveRecord::Base # Since we do cache @event we need to reset cache in special cases: # * when project was moved # * when project was renamed + # * when the project avatar changes # Events cache stored like events/23-20130109142513. # The cache key includes updated_at timestamp. # Thus it will automatically generate a new fragment diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 4930660055..8bb0fcf947 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -14,6 +14,9 @@ module Projects project.name = @from_project.name project.path = @from_project.path project.creator = @current_user + if @from_project.avatar && @from_project.avatar.image? + project.avatar = @from_project.avatar + end if namespace = @params[:namespace] project.namespace = namespace diff --git a/app/views/dashboard/_project.html.haml b/app/views/dashboard/_project.html.haml index 89ed510275..7f19fb5a81 100644 --- a/app/views/dashboard/_project.html.haml +++ b/app/views/dashboard/_project.html.haml @@ -1,4 +1,6 @@ = link_to project_path(project), class: dom_class(project) do + .dash-project-avatar + = project_icon(project.to_param, alt: '', class: 'avatar s24') .dash-project-access-icon = visibility_level_icon(project.visibility_level) %span.str-truncated diff --git a/app/views/dashboard/projects.html.haml b/app/views/dashboard/projects.html.haml index 944441669e..f60bcc72e1 100644 --- a/app/views/dashboard/projects.html.haml +++ b/app/views/dashboard/projects.html.haml @@ -11,6 +11,8 @@ - @projects.each do |project| %li.my-project-row %h4.project-title + .project-avatar + = project_icon(project.to_param, alt: '', class: 'avatar s60') .project-access-icon = visibility_level_icon(project.visibility_level) = link_to project_path(project), class: dom_class(project) do diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 30d063c7a3..05910c6038 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -2,6 +2,7 @@ .project-home-panel{:class => ("empty-project" if empty_repo)} .project-home-row .project-home-desc + = project_icon(@project.to_param, alt: '', class: 'avatar s32') - if @project.description.present? = escaped_autolink(@project.description) - if can?(current_user, :admin_project, @project) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index f2bb56b566..fc6499de3e 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -7,7 +7,8 @@ %p.light Some settings, such as "Transfer Project", are hidden inside the danger area below. %hr .panel-body - = form_for @project, remote: true, html: { class: "edit_project form-horizontal" } do |f| + = form_for @project, remote: true, html: { multipart: true, class: "edit_project form-horizontal" }, authenticity_token: true do |f| + %fieldset .form-group.project_name_holder = f.label :name, class: 'control-label' do @@ -80,6 +81,31 @@ = f.check_box :snippets_enabled %span.descr Share code pastes with others out of git repository + %fieldset.features + %legend + Project avatar: + .form-group + .col-sm-2 + .col-sm-10 + = project_icon(@project.to_param, alt: '', class: 'avatar s160', only_uploaded: true) + %p.light + - if @project.avatar_in_git + Project avatar in repository: #{ @project.avatar_in_git } + %p.light + - if @project.avatar? + You can change your project avatar here + - else + You can upload an project avatar here + %a.choose-btn.btn.btn-small.js-choose-project-avatar-button + %i.icon-paper-clip + %span Choose File ... +   + %span.file_name.js-avatar-filename File name... + = f.file_field :avatar, class: "js-project-avatar-input hidden" + .light The maximum file size allowed is 100KB. + - if @project.avatar? + %hr + = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar" .form-actions = f.submit 'Save changes', class: "btn btn-save" diff --git a/config/routes.rb b/config/routes.rb index ef3c5aedfc..32378665c9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -353,6 +353,8 @@ Gitlab::Application.routes.draw do delete :delete_attachment end end + + resource :avatar, only: [:show, :destroy] end end diff --git a/db/migrate/20140125162722_add_avatar_to_projects.rb b/db/migrate/20140125162722_add_avatar_to_projects.rb new file mode 100644 index 0000000000..9523ac722f --- /dev/null +++ b/db/migrate/20140125162722_add_avatar_to_projects.rb @@ -0,0 +1,5 @@ +class AddAvatarToProjects < ActiveRecord::Migration + def change + add_column :projects, :avatar, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index b453164d71..29466f048e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -327,6 +327,7 @@ ActiveRecord::Schema.define(version: 20150116234544) do t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" + t.string "avatar" end add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree diff --git a/features/project/project.feature b/features/project/project.feature index 7bb24e013a..3e1fd54bee 100644 --- a/features/project/project.feature +++ b/features/project/project.feature @@ -5,6 +5,19 @@ Feature: Project And project "Shop" has push event And I visit project "Shop" page + Scenario: I edit the project avatar + Given I visit edit project "Shop" page + When I change the project avatar + And I should see new project avatar + And I should see the "Remove avatar" button + + Scenario: I remove the project avatar + Given I visit edit project "Shop" page + And I have an project avatar + When I remove my project avatar + Then I should see the default project avatar + And I should not see the "Remove avatar" button + @javascript Scenario: I should see project activity When I visit project "Shop" page diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 5e7312d90f..455539d747 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -17,12 +17,53 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'change project path settings' do - fill_in "project_path", with: "new-path" - click_button "Rename" + fill_in 'project_path', with: 'new-path' + click_button 'Rename' end step 'I should see project with new path settings' do - project.path.should == "new-path" + project.path.should == 'new-path' + end + + step 'I change the project avatar' do + attach_file( + :project_avatar, + File.join(Rails.root, 'public', 'gitlab_logo.png') + ) + click_button 'Save changes' + @project.reload + end + + step 'I should see new project avatar' do + @project.avatar.should be_instance_of AttachmentUploader + url = @project.avatar.url + url.should == "/uploads/project/avatar/#{ @project.id }/gitlab_logo.png" + end + + step 'I should see the "Remove avatar" button' do + page.should have_link('Remove avatar') + end + + step 'I have an project avatar' do + attach_file( + :project_avatar, + File.join(Rails.root, 'public', 'gitlab_logo.png') + ) + click_button 'Save changes' + @project.reload + end + + step 'I remove my project avatar' do + click_link 'Remove avatar' + @project.reload + end + + step 'I should see the default project avatar' do + @project.avatar?.should be_false + end + + step 'I should not see the "Remove avatar" button' do + page.should_not have_link('Remove avatar') end step 'I should see project "Shop" version' do diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 1738f3443c..ed50bf7c75 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -56,6 +56,28 @@ describe ApplicationHelper do end end + describe 'project_icon' do + avatar_file_path = File.join(Rails.root, 'public', 'gitlab_logo.png') + + it 'should return an url for the avatar' do + project = create(:project) + project.avatar = File.open(avatar_file_path) + project.save! + project_icon(project.to_param).to_s.should == + "/uploads/project/avatar/#{ project.id }/gitlab_logo.png" + end + + it "should give uploaded icon when present" do + project = create(:project) + project.save! + + Project.any_instance.stub(:avatar_in_git).and_return(true) + + project_icon(project.to_param).to_s.should match( + image_tag(project_avatar_path(project))) + end + end + describe "avatar_icon" do avatar_file_path = File.join(Rails.root, 'public', 'gitlab_logo.png') diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2a27817637..87d26f98b4 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -26,6 +26,7 @@ # star_count :integer default(0), not null # import_type :string(255) # import_source :string(255) +# avatar :string(255) # require 'spec_helper' @@ -310,4 +311,18 @@ describe Project do expect(project.star_count).to eq(0) end end + + describe :avatar_type do + let(:project) { create(:project) } + + it 'should be true if avatar is image' do + project.update_attribute(:avatar, 'uploads/avatar.png') + project.avatar_type.should be_true + end + + it 'should be false if avatar is html page' do + project.update_attribute(:avatar, 'uploads/avatar.html') + project.avatar_type.should == ['only images allowed'] + end + end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index f149f3f62a..67705c6cb4 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -489,4 +489,11 @@ describe Projects::ForksController, "routing" do it "to #create" do post("/gitlab/gitlabhq/fork").should route_to("projects/forks#create", project_id: 'gitlab/gitlabhq') end + +# project_avatar DELETE /project/avatar(.:format) projects/avatars#destroy +describe Projects::AvatarsController, 'routing' do + it 'to #destroy' do + delete('/gitlab/gitlabhq/avatar').should route_to( + 'projects/avatars#destroy', project_id: 'gitlab/gitlabhq') + end end From 70c44a0da2bdeead90a99fe79e7c047d38b8ca5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Mon, 19 Jan 2015 21:37:20 +0100 Subject: [PATCH 058/124] Fix tests, merge conflicts, some minor issues and make the project avatar feature mergable --- CHANGELOG | 2 +- app/assets/javascripts/dispatcher.js.coffee | 1 + app/assets/javascripts/project.js.coffee | 10 - .../javascripts/project_avatar.js.coffee | 9 + app/controllers/projects_controller.rb | 20 +- app/helpers/application_helper.rb | 36 +- app/models/project.rb | 77 ++-- app/services/projects/fork_service.rb | 8 +- app/views/groups/_projects.html.haml | 2 + app/views/projects/edit.html.haml | 7 +- config/routes.rb | 44 +-- features/steps/project/project.rb | 10 +- spec/helpers/application_helper_spec.rb | 106 +++--- spec/models/project_spec.rb | 58 +-- spec/routing/project_routing_spec.rb | 335 +++++++++--------- 15 files changed, 367 insertions(+), 358 deletions(-) create mode 100644 app/assets/javascripts/project_avatar.js.coffee diff --git a/CHANGELOG b/CHANGELOG index 06a10e379f..dd9b13ceac 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -34,7 +34,7 @@ v 7.8.0 - - - - - + - Add Project Avatars (Steven Thonus and Hannes Rosenögger) - - - Password reset token validity increased from 2 hours to 2 days since it is also send on account creation. diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index ef86c2781c..1643ca941f 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -96,6 +96,7 @@ class Dispatcher new Profile() when 'projects' new Project() + new ProjectAvatar() switch path[1] when 'edit' shortcut_handler = new ShortcutsNavigation() diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index 8588a9d27c..5a9cc66c8f 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -18,13 +18,3 @@ class @Project $.cookie('hide_no_ssh_message', 'false', { path: path }) $(@).parents('.no-ssh-key-message').hide() e.preventDefault() - - # avatar - $('.js-choose-project-avatar-button').bind "click", -> - form = $(this).closest("form") - form.find(".js-project-avatar-input").click() - - $('.js-project-avatar-input').bind "change", -> - form = $(this).closest("form") - filename = $(this).val().replace(/^.*[\\\/]/, '') - form.find(".js-avatar-filename").text(filename) diff --git a/app/assets/javascripts/project_avatar.js.coffee b/app/assets/javascripts/project_avatar.js.coffee new file mode 100644 index 0000000000..8bec6e2ccc --- /dev/null +++ b/app/assets/javascripts/project_avatar.js.coffee @@ -0,0 +1,9 @@ +class @ProjectAvatar + constructor: -> + $('.js-choose-project-avatar-button').bind 'click', -> + form = $(this).closest('form') + form.find('.js-project-avatar-input').click() + $('.js-project-avatar-input').bind 'change', -> + form = $(this).closest('form') + filename = $(this).val().replace(/^.*[\\\/]/, '') + form.find('.js-avatar-filename').text(filename) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 89296b9aa4..ebe48265c6 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -14,7 +14,7 @@ class ProjectsController < ApplicationController end def edit - render 'edit', layout: "project_settings" + render 'edit', layout: 'project_settings' end def create @@ -36,7 +36,7 @@ class ProjectsController < ApplicationController format.html { redirect_to edit_project_path(@project), notice: 'Project was successfully updated.' } format.js else - format.html { render "edit", layout: "project_settings" } + format.html { render 'edit', layout: 'project_settings' } format.js end end @@ -66,17 +66,17 @@ class ProjectsController < ApplicationController format.html do if @project.repository_exists? if @project.empty_repo? - render "projects/empty", layout: user_layout + render 'projects/empty', layout: user_layout else @last_push = current_user.recent_push(@project.id) if current_user render :show, layout: user_layout end else - render "projects/no_repo", layout: user_layout + render 'projects/no_repo', layout: user_layout end end - format.json { pager_json("events/_events", @events.count) } + format.json { pager_json('events/_events', @events.count) } end end @@ -87,9 +87,9 @@ class ProjectsController < ApplicationController respond_to do |format| format.html do - flash[:alert] = "Project deleted." + flash[:alert] = 'Project deleted.' - if request.referer.include?("/admin") + if request.referer.include?('/admin') redirect_to admin_projects_path else redirect_to projects_dashboard_path @@ -141,7 +141,7 @@ class ProjectsController < ApplicationController if link_to_image format.json { render json: { link: link_to_image } } else - format.json { render json: "Invalid file.", status: :unprocessable_entity } + format.json { render json: 'Invalid file.', status: :unprocessable_entity } end end end @@ -172,14 +172,14 @@ class ProjectsController < ApplicationController end def user_layout - current_user ? "projects" : "public_projects" + current_user ? 'projects' : 'public_projects' end def project_params params.require(:project).permit( :name, :path, :description, :issues_tracker, :tag_list, :issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch, - :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id + :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar ) end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 772400d55e..32fd0ed7bc 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -54,10 +54,10 @@ module ApplicationHelper project = Project.find_with_namespace(project_id) if project.avatar.present? image_tag project.avatar.url, options - elsif options[:only_uploaded] - image_tag '/assets/no_project_icon.png', options elsif project.avatar_in_git image_tag project_avatar_path(project), options + elsif options[:only_uploaded] + image_tag '/assets/no_project_icon.png', options else # generated icon project_identicon(project, options) end @@ -107,24 +107,24 @@ module ApplicationHelper if project.repo_exists? time_ago_with_tooltip(project.repository.commit.committed_date) else - "Never" + 'Never' end rescue - "Never" + 'Never' end def grouped_options_refs repository = @project.repository options = [ - ["Branches", repository.branch_names], - ["Tags", VersionSorter.rsort(repository.tag_names)] + ['Branches', repository.branch_names], + ['Tags', VersionSorter.rsort(repository.tag_names)] ] # If reference is commit id - we should add it to branch/tag selectbox if(@ref && !options.flatten.include?(@ref) && @ref =~ /^[0-9a-zA-Z]{6,52}$/) - options << ["Commit", [@ref]] + options << ['Commit', [@ref]] end grouped_options_for_select(options, @ref || @project.default_branch) @@ -186,7 +186,7 @@ module ApplicationHelper path = controller.controller_path.split('/') namespace = path.first if path.second - [namespace, controller.controller_name, controller.action_name].compact.join(":") + [namespace, controller.controller_name, controller.action_name].compact.join(':') end # shortcut for gitlab config @@ -201,13 +201,13 @@ module ApplicationHelper def search_placeholder if @project && @project.persisted? - "Search in this project" + 'Search in this project' elsif @snippet || @snippets || @show_snippets 'Search snippets' elsif @group && @group.persisted? - "Search in this group" + 'Search in this group' else - "Search" + 'Search' end end @@ -218,7 +218,7 @@ module ApplicationHelper def time_ago_with_tooltip(date, placement = 'top', html_class = 'time_ago') capture_haml do haml_tag :time, date.to_s, - class: html_class, datetime: date.getutc.iso8601, title: date.stamp("Aug 21, 2011 9:23pm"), + class: html_class, datetime: date.getutc.iso8601, title: date.stamp('Aug 21, 2011 9:23pm'), data: { toggle: 'tooltip', placement: placement } haml_tag :script, "$('." + html_class + "').timeago().tooltip()" @@ -241,8 +241,8 @@ module ApplicationHelper end def spinner(text = nil, visible = false) - css_class = "loading" - css_class << " hide" unless visible + css_class = 'loading' + css_class << ' hide' unless visible content_tag :div, class: css_class do content_tag(:i, nil, class: 'fa fa-spinner fa-spin') + text @@ -259,17 +259,17 @@ module ApplicationHelper absolute_uri = nil end - # Add "nofollow" only to external links + # Add 'nofollow' only to external links if host && host != Gitlab.config.gitlab.host && absolute_uri if html_options if html_options[:rel] - html_options[:rel] << " nofollow" + html_options[:rel] << ' nofollow' else - html_options.merge!(rel: "nofollow") + html_options.merge!(rel: 'nofollow') end else html_options = Hash.new - html_options[:rel] = "nofollow" + html_options[:rel] = 'nofollow' end end diff --git a/app/models/project.rb b/app/models/project.rb index 7160e704aa..97f2322748 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -14,7 +14,7 @@ # merge_requests_enabled :boolean default(TRUE), not null # wiki_enabled :boolean default(TRUE), not null # namespace_id :integer -# issues_tracker :string(255) default("gitlab"), not null +# issues_tracker :string(255) default('gitlab'), not null # issues_tracker_id :string(255) # snippets_enabled :boolean default(TRUE), not null # last_activity_at :datetime @@ -29,6 +29,9 @@ # avatar :string(255) # +require 'carrierwave/orm/activerecord' +require 'file_size_validator' + class Project < ActiveRecord::Base include Gitlab::ShellAdapter include Gitlab::VisibilityLevel @@ -50,8 +53,8 @@ class Project < ActiveRecord::Base attr_accessor :new_default_branch # Relations - belongs_to :creator, foreign_key: "creator_id", class_name: "User" - belongs_to :group, -> { where(type: Group) }, foreign_key: "namespace_id" + belongs_to :creator, foreign_key: 'creator_id', class_name: 'User' + belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id' belongs_to :namespace has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id' @@ -71,20 +74,20 @@ class Project < ActiveRecord::Base has_one :bamboo_service, dependent: :destroy has_one :teamcity_service, dependent: :destroy has_one :pushover_service, dependent: :destroy - has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" + has_one :forked_project_link, dependent: :destroy, foreign_key: 'forked_to_project_id' has_one :forked_from_project, through: :forked_project_link # Merge Requests for target project should be removed with it - has_many :merge_requests, dependent: :destroy, foreign_key: "target_project_id" + has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id' # Merge requests from source project should be kept when source project was removed - has_many :fork_merge_requests, foreign_key: "source_project_id", class_name: MergeRequest + has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest has_many :issues, -> { order 'issues.state DESC, issues.created_at DESC' }, dependent: :destroy has_many :labels, dependent: :destroy has_many :services, dependent: :destroy has_many :events, dependent: :destroy has_many :milestones, dependent: :destroy has_many :notes, dependent: :destroy - has_many :snippets, dependent: :destroy, class_name: "ProjectSnippet" - has_many :hooks, dependent: :destroy, class_name: "ProjectHook" + has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet' + has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :protected_branches, dependent: :destroy has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember' has_many :users, through: :project_members @@ -116,27 +119,27 @@ class Project < ActiveRecord::Base validates_uniqueness_of :name, scope: :namespace_id validates_uniqueness_of :path, scope: :namespace_id validates :import_url, - format: { with: URI::regexp(%w(git http https)), message: "should be a valid url" }, + format: { with: URI::regexp(%w(git http https)), message: 'should be a valid url' }, if: :import? validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create validate :avatar_type, if: ->(project) { project.avatar && project.avatar_changed? } - validates :avatar, file_size: { maximum: 100.kilobytes.to_i } + validates :avatar, file_size: { maximum: 200.kilobytes.to_i } mount_uploader :avatar, AttachmentUploader # Scopes - scope :without_user, ->(user) { where("projects.id NOT IN (:ids)", ids: user.authorized_projects.map(&:id) ) } - scope :without_team, ->(team) { team.projects.present? ? where("projects.id NOT IN (:ids)", ids: team.projects.map(&:id)) : scoped } - scope :not_in_group, ->(group) { where("projects.id NOT IN (:ids)", ids: group.project_ids ) } - scope :in_team, ->(team) { where("projects.id IN (:ids)", ids: team.projects.map(&:id)) } + scope :without_user, ->(user) { where('projects.id NOT IN (:ids)', ids: user.authorized_projects.map(&:id) ) } + scope :without_team, ->(team) { team.projects.present? ? where('projects.id NOT IN (:ids)', ids: team.projects.map(&:id)) : scoped } + scope :not_in_group, ->(group) { where('projects.id NOT IN (:ids)', ids: group.project_ids ) } + scope :in_team, ->(team) { where('projects.id IN (:ids)', ids: team.projects.map(&:id)) } scope :in_namespace, ->(namespace) { where(namespace_id: namespace.id) } scope :in_group_namespace, -> { joins(:group) } - scope :sorted_by_activity, -> { reorder("projects.last_activity_at DESC") } - scope :sorted_by_stars, -> { reorder("projects.star_count DESC") } + scope :sorted_by_activity, -> { reorder('projects.last_activity_at DESC') } + scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } scope :personal, ->(user) { where(namespace_id: user.namespace_id) } - scope :joined, ->(user) { where("namespace_id != ?", user.namespace_id) } + scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) } scope :public_only, -> { where(visibility_level: Project::PUBLIC) } scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) } scope :non_archived, -> { where(archived: false) } @@ -187,26 +190,26 @@ class Project < ActiveRecord::Base end def active - joins(:issues, :notes, :merge_requests).order("issues.created_at, notes.created_at, merge_requests.created_at DESC") + joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') end def search(query) - joins(:namespace).where("projects.archived = ?", false). - where("LOWER(projects.name) LIKE :query OR + joins(:namespace).where('projects.archived = ?', false). + where('LOWER(projects.name) LIKE :query OR LOWER(projects.path) LIKE :query OR LOWER(namespaces.name) LIKE :query OR - LOWER(projects.description) LIKE :query", + LOWER(projects.description) LIKE :query', query: "%#{query.try(:downcase)}%") end def search_by_title(query) - where("projects.archived = ?", false).where("LOWER(projects.name) LIKE :query", query: "%#{query.downcase}%") + where('projects.archived = ?', false).where('LOWER(projects.name) LIKE :query', query: "%#{query.downcase}%") end def find_with_namespace(id) - return nil unless id.include?("/") + return nil unless id.include?('/') - id = id.split("/") + id = id.split('/') namespace = Namespace.find_by(path: id.first) return nil unless namespace @@ -224,7 +227,7 @@ class Project < ActiveRecord::Base when 'recently_updated' then reorder('projects.updated_at DESC') when 'last_updated' then reorder('projects.updated_at ASC') when 'largest_repository' then reorder('projects.repository_size DESC') - else reorder("namespaces.path, projects.name ASC") + else reorder('namespaces.path, projects.name ASC') end end end @@ -274,19 +277,19 @@ class Project < ActiveRecord::Base end def to_param - namespace.path + "/" + path + namespace.path + '/' + path end def web_url - [gitlab_config.url, path_with_namespace].join("/") + [gitlab_config.url, path_with_namespace].join('/') end def web_url_without_protocol - web_url.split("://")[1] + web_url.split('://')[1] end def build_commit_note(commit) - notes.new(commit_id: commit.id, noteable_type: "Commit") + notes.new(commit_id: commit.id, noteable_type: 'Commit') end def last_activity @@ -345,8 +348,8 @@ class Project < ActiveRecord::Base end def avatar_type - unless avatar.image? - errors.add :avatar, 'only images allowed' + unless self.avatar.image? + self.errors.add :avatar, 'only images allowed' end end @@ -384,7 +387,7 @@ class Project < ActiveRecord::Base end def team_member_by_name_or_email(name = nil, email = nil) - user = users.where("name like ? or email like ?", name, email).first + user = users.where('name like ? or email like ?', name, email).first project_members.where(user: user) if user end @@ -396,7 +399,7 @@ class Project < ActiveRecord::Base def name_with_namespace @name_with_namespace ||= begin if namespace - namespace.human_name + " / " + name + namespace.human_name + ' / ' + name else name end @@ -431,7 +434,7 @@ class Project < ActiveRecord::Base def valid_repo? repository.exists? rescue - errors.add(:path, "Invalid repository path") + errors.add(:path, 'Invalid repository path') false end @@ -490,7 +493,7 @@ class Project < ActiveRecord::Base end def http_url_to_repo - [gitlab_config.url, "/", path_with_namespace, ".git"].join('') + [gitlab_config.url, '/', path_with_namespace, '.git'].join('') end # Check if current branch name is marked as protected in the system @@ -618,7 +621,7 @@ class Project < ActiveRecord::Base if gitlab_shell.add_repository(path_with_namespace) true else - errors.add(:base, "Failed to create repository") + errors.add(:base, 'Failed to create repository') false end end @@ -631,7 +634,7 @@ class Project < ActiveRecord::Base ProjectWiki.new(self, self.owner).wiki true rescue ProjectWiki::CouldNotCreateWikiError => ex - errors.add(:base, "Failed create wiki") + errors.add(:base, 'Failed create wiki') false end end diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index 8bb0fcf947..6b0d4aca3e 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -14,7 +14,7 @@ module Projects project.name = @from_project.name project.path = @from_project.path project.creator = @current_user - if @from_project.avatar && @from_project.avatar.image? + if @from_project.avatar.present? && @from_project.avatar.image? project.avatar = @from_project.avatar end @@ -42,16 +42,16 @@ module Projects end #Now fork the repo unless gitlab_shell.fork_repository(@from_project.path_with_namespace, project.namespace.path) - raise "forking failed in gitlab-shell" + raise 'forking failed in gitlab-shell' end project.ensure_satellite_exists end rescue => ex - project.errors.add(:base, "Fork transaction failed.") + project.errors.add(:base, 'Fork transaction failed.') project.destroy end else - project.errors.add(:base, "Invalid fork destination") + project.errors.add(:base, 'Invalid fork destination') end project diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 2c65b3049e..2716ebf326 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -12,6 +12,8 @@ - projects.each do |project| %li.project-row = link_to project_path(project), class: dom_class(project) do + .dash-project-avatar + = project_icon(project.to_param, alt: '', class: 'avatar s24') .dash-project-access-icon = visibility_level_icon(project.visibility_level) %span.str-truncated diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index fc6499de3e..28de1a778a 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -87,7 +87,10 @@ .form-group .col-sm-2 .col-sm-10 - = project_icon(@project.to_param, alt: '', class: 'avatar s160', only_uploaded: true) + - if @project.avatar? + = project_icon(@project.to_param, alt: '', class: 'avatar s160') + - else + = project_icon(@project.to_param, alt: '', class: 'avatar s160', only_uploaded: true) %p.light - if @project.avatar_in_git Project avatar in repository: #{ @project.avatar_in_git } @@ -102,7 +105,7 @@   %span.file_name.js-avatar-filename File name... = f.file_field :avatar, class: "js-project-avatar-input hidden" - .light The maximum file size allowed is 100KB. + .light The maximum file size allowed is 200KB. - if @project.avatar? %hr = link_to 'Remove avatar', project_avatar_path(@project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar" diff --git a/config/routes.rb b/config/routes.rb index 32378665c9..3ee4f07ec7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,8 +10,8 @@ Gitlab::Application.routes.draw do # # Search # - get 'search' => "search#show" - get 'search/autocomplete' => "search#autocomplete", as: :search_autocomplete + get 'search' => 'search#show' + get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete # API API::API.logger Rails.logger @@ -20,9 +20,9 @@ Gitlab::Application.routes.draw do # Get all keys of user get ':username.keys' => 'profiles/keys#get_keys' , constraints: { username: /.*/ } - constraint = lambda { |request| request.env["warden"].authenticate? and request.env['warden'].user.admin? } + constraint = lambda { |request| request.env['warden'].authenticate? and request.env['warden'].user.admin? } constraints constraint do - mount Sidekiq::Web, at: "/admin/sidekiq", as: :sidekiq + mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq end # Enable Grack support @@ -46,10 +46,10 @@ Gitlab::Application.routes.draw do # resources :snippets do member do - get "raw" + get 'raw' end end - get "/s/:username" => "snippets#user_index", as: :user_snippets, constraints: { username: /.*/ } + get '/s/:username' => 'snippets#user_index', as: :user_snippets, constraints: { username: /.*/ } # # Github importer area @@ -72,12 +72,12 @@ Gitlab::Application.routes.draw do end resources :groups, only: [:index] - root to: "projects#trending" + root to: 'projects#trending' end # Compatibility with old routing - get 'public' => "explore/projects#index" - get 'public/projects' => "explore/projects#index" + get 'public' => 'explore/projects#index' + get 'public/projects' => 'explore/projects#index' # # Attachments serving @@ -122,7 +122,7 @@ Gitlab::Application.routes.draw do resource :application_settings, only: [:show, :update] - root to: "dashboard#index" + root to: 'dashboard#index' end # @@ -163,7 +163,7 @@ Gitlab::Application.routes.draw do # # Dashboard Area # - resource :dashboard, controller: "dashboard", only: [:show] do + resource :dashboard, controller: 'dashboard', only: [:show] do member do get :projects get :issues @@ -194,12 +194,12 @@ Gitlab::Application.routes.draw do devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, registrations: :registrations , passwords: :passwords, sessions: :sessions, confirmations: :confirmations } devise_scope :user do - get "/users/auth/:provider/omniauth_error" => "omniauth_callbacks#omniauth_error", as: :omniauth_error + get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error end # # Project Area # - resources :projects, constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/ }, except: [:new, :create, :index], path: "/" do + resources :projects, constraints: { id: /[a-zA-Z.0-9_\-]+\/[a-zA-Z.0-9_\-]+/ }, except: [:new, :create, :index], path: '/' do member do put :transfer post :archive @@ -220,6 +220,7 @@ Gitlab::Application.routes.draw do # Cannot be GET to differentiate from GET paths that end in preview. post :preview, on: :member end + resource :avatar, only: [:show, :destroy] resources :new_tree, only: [:show, :update], constraints: {id: /.+/}, path: 'new' resources :commit, only: [:show], constraints: {id: /[[:alnum:]]{6,40}/} resources :commits, only: [:show], constraints: {id: /(?:[^.]|\.(?!atom$))+/, format: /atom/} @@ -237,7 +238,7 @@ Gitlab::Application.routes.draw do resources :snippets, constraints: {id: /\d+/} do member do - get "raw" + get 'raw' end end @@ -249,7 +250,7 @@ Gitlab::Application.routes.draw do end member do - get "history" + get 'history' end end @@ -258,7 +259,7 @@ Gitlab::Application.routes.draw do resource :repository, only: [:show, :create] do member do - get "archive", constraints: { format: Gitlab::Regex.archive_formats_regex } + get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex } end end @@ -281,13 +282,13 @@ Gitlab::Application.routes.draw do resources :refs, only: [] do collection do - get "switch" + get 'switch' end member do # tree viewer logs - get "logs_tree", constraints: { id: Gitlab::Regex.git_reference_regex } - get "logs_tree/:path" => "refs#logs_tree", + get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex } + get 'logs_tree/:path' => 'refs#logs_tree', as: :logs_file, constraints: { id: Gitlab::Regex.git_reference_regex, @@ -354,11 +355,10 @@ Gitlab::Application.routes.draw do end end - resource :avatar, only: [:show, :destroy] end end - get ':id' => "namespaces#show", constraints: {id: /(?:[^.]|\.(?!atom$))+/, format: /atom/} + get ':id' => 'namespaces#show', constraints: {id: /(?:[^.]|\.(?!atom$))+/, format: /atom/} - root to: "dashboard#show" + root to: 'dashboard#show' end diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 455539d747..033d45e025 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -68,7 +68,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps step 'I should see project "Shop" version' do within '.project-side' do - page.should have_content "Version: 6.7.0.pre" + page.should have_content 'Version: 6.7.0.pre' end end @@ -86,12 +86,12 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see project "Forum" README' do - page.should have_link "README.md" - page.should have_content "Sample repo for testing gitlab features" + page.should have_link 'README.md' + page.should have_content 'Sample repo for testing gitlab features' end step 'I should see project "Shop" README' do - page.should have_link "README.md" - page.should have_content "testme" + page.should have_link 'README.md' + page.should have_content 'testme' end end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index ed50bf7c75..a46883b3c9 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -6,15 +6,15 @@ describe ApplicationHelper do controller.stub(:controller_name).and_return('foo') end - it "returns true when controller matches argument" do + it 'returns true when controller matches argument' do current_controller?(:foo).should be_true end - it "returns false when controller does not match argument" do + it 'returns false when controller does not match argument' do current_controller?(:bar).should_not be_true end - it "should take any number of arguments" do + it 'should take any number of arguments' do current_controller?(:baz, :bar).should_not be_true current_controller?(:baz, :bar, :foo).should be_true end @@ -25,34 +25,34 @@ describe ApplicationHelper do allow(self).to receive(:action_name).and_return('foo') end - it "returns true when action matches argument" do + it 'returns true when action matches argument' do current_action?(:foo).should be_true end - it "returns false when action does not match argument" do + it 'returns false when action does not match argument' do current_action?(:bar).should_not be_true end - it "should take any number of arguments" do + it 'should take any number of arguments' do current_action?(:baz, :bar).should_not be_true current_action?(:baz, :bar, :foo).should be_true end end - describe "group_icon" do + describe 'group_icon' do avatar_file_path = File.join(Rails.root, 'public', 'gitlab_logo.png') - it "should return an url for the avatar" do + it 'should return an url for the avatar' do group = create(:group) group.avatar = File.open(avatar_file_path) group.save! group_icon(group.path).to_s.should match("/uploads/group/avatar/#{ group.id }/gitlab_logo.png") end - it "should give default avatar_icon when no avatar is present" do + it 'should give default avatar_icon when no avatar is present' do group = create(:group) group.save! - group_icon(group.path).should match("group_avatar.png") + group_icon(group.path).should match('group_avatar.png') end end @@ -64,10 +64,10 @@ describe ApplicationHelper do project.avatar = File.open(avatar_file_path) project.save! project_icon(project.to_param).to_s.should == - "/uploads/project/avatar/#{ project.id }/gitlab_logo.png" + "\"Gitlab" end - it "should give uploaded icon when present" do + it 'should give uploaded icon when present' do project = create(:project) project.save! @@ -78,18 +78,18 @@ describe ApplicationHelper do end end - describe "avatar_icon" do + describe 'avatar_icon' do avatar_file_path = File.join(Rails.root, 'public', 'gitlab_logo.png') - it "should return an url for the avatar" do + it 'should return an url for the avatar' do user = create(:user) user.avatar = File.open(avatar_file_path) user.save! avatar_icon(user.email).to_s.should match("/uploads/user/avatar/#{ user.id }/gitlab_logo.png") end - it "should return an url for the avatar with relative url" do - Gitlab.config.gitlab.stub(relative_url_root: "/gitlab") + it 'should return an url for the avatar with relative url' do + Gitlab.config.gitlab.stub(relative_url_root: '/gitlab') Gitlab.config.gitlab.stub(url: Settings.send(:build_gitlab_url)) user = create(:user) @@ -98,58 +98,58 @@ describe ApplicationHelper do avatar_icon(user.email).to_s.should match("/gitlab/uploads/user/avatar/#{ user.id }/gitlab_logo.png") end - it "should call gravatar_icon when no avatar is present" do + it 'should call gravatar_icon when no avatar is present' do user = create(:user, email: 'test@example.com') user.save! - avatar_icon(user.email).to_s.should == "http://www.gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=40&d=identicon" + avatar_icon(user.email).to_s.should == 'http://www.gravatar.com/avatar/55502f40dc8b7c769880b10874abc9d0?s=40&d=identicon' end end - describe "gravatar_icon" do + describe 'gravatar_icon' do let(:user_email) { 'user@email.com' } - it "should return a generic avatar path when Gravatar is disabled" do + it 'should return a generic avatar path when Gravatar is disabled' do ApplicationSetting.any_instance.stub(gravatar_enabled?: false) gravatar_icon(user_email).should match('no_avatar.png') end - it "should return a generic avatar path when email is blank" do + it 'should return a generic avatar path when email is blank' do gravatar_icon('').should match('no_avatar.png') end - it "should return default gravatar url" do + it 'should return default gravatar url' do Gitlab.config.gitlab.stub(https: false) gravatar_icon(user_email).should match('http://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118') end - it "should use SSL when appropriate" do + it 'should use SSL when appropriate' do Gitlab.config.gitlab.stub(https: true) gravatar_icon(user_email).should match('https://secure.gravatar.com') end - it "should return custom gravatar path when gravatar_url is set" do + it 'should return custom gravatar path when gravatar_url is set' do allow(self).to receive(:request).and_return(double(:ssl? => false)) Gitlab.config.gravatar.stub(:plain_url).and_return('http://example.local/?s=%{size}&hash=%{hash}') gravatar_icon(user_email, 20).should == 'http://example.local/?s=20&hash=b58c6f14d292556214bd64909bcdb118' end - it "should accept a custom size" do + it 'should accept a custom size' do allow(self).to receive(:request).and_return(double(:ssl? => false)) gravatar_icon(user_email, 64).should match(/\?s=64/) end - it "should use default size when size is wrong" do + it 'should use default size when size is wrong' do allow(self).to receive(:request).and_return(double(:ssl? => false)) gravatar_icon(user_email, nil).should match(/\?s=40/) end - it "should be case insensitive" do + it 'should be case insensitive' do allow(self).to receive(:request).and_return(double(:ssl? => false)) - gravatar_icon(user_email).should == gravatar_icon(user_email.upcase + " ") + gravatar_icon(user_email).should == gravatar_icon(user_email.upcase + ' ') end end - describe "grouped_options_refs" do + describe 'grouped_options_refs' do # Override Rails' grouped_options_for_select helper since HTML is harder to work with def grouped_options_for_select(options, *args) options @@ -162,17 +162,17 @@ describe ApplicationHelper do @project = create(:project) end - it "includes a list of branch names" do + it 'includes a list of branch names' do options[0][0].should == 'Branches' options[0][1].should include('master', 'feature') end - it "includes a list of tag names" do + it 'includes a list of tag names' do options[1][0].should == 'Tags' options[1][1].should include('v1.0.0','v1.1.0') end - it "includes a specific commit ref if defined" do + it 'includes a specific commit ref if defined' do # Must be an instance variable @ref = '2ed06dc41dbb5936af845b87d79e05bbf24c73b8' @@ -180,26 +180,26 @@ describe ApplicationHelper do options[2][1].should == [@ref] end - it "sorts tags in a natural order" do + it 'sorts tags in a natural order' do # Stub repository.tag_names to make sure we get some valid testing data - expect(@project.repository).to receive(:tag_names).and_return(["v1.0.9", "v1.0.10", "v2.0", "v3.1.4.2", "v1.0.9a"]) + expect(@project.repository).to receive(:tag_names).and_return(['v1.0.9', 'v1.0.10', 'v2.0', 'v3.1.4.2', 'v1.0.9a']) - options[1][1].should == ["v3.1.4.2", "v2.0", "v1.0.10", "v1.0.9a", "v1.0.9"] + options[1][1].should == ['v3.1.4.2', 'v2.0', 'v1.0.10', 'v1.0.9a', 'v1.0.9'] end end - describe "user_color_scheme_class" do - context "with current_user is nil" do - it "should return a string" do + describe 'user_color_scheme_class' do + context 'with current_user is nil' do + it 'should return a string' do allow(self).to receive(:current_user).and_return(nil) user_color_scheme_class.should be_kind_of(String) end end - context "with a current_user" do + context 'with a current_user' do (1..5).each do |color_scheme_id| context "with color_scheme_id == #{color_scheme_id}" do - it "should return a string" do + it 'should return a string' do current_user = double(:color_scheme_id => color_scheme_id) allow(self).to receive(:current_user).and_return(current_user) user_color_scheme_class.should be_kind_of(String) @@ -209,43 +209,43 @@ describe ApplicationHelper do end end - describe "simple_sanitize" do + describe 'simple_sanitize' do let(:a_tag) { 'Foo' } - it "allows the a tag" do + it 'allows the a tag' do simple_sanitize(a_tag).should == a_tag end - it "allows the span tag" do + it 'allows the span tag' do input = 'Bar' simple_sanitize(input).should == input end - it "disallows other tags" do + it 'disallows other tags' do input = "#{a_tag}" simple_sanitize(input).should == a_tag end end - describe "link_to" do + describe 'link_to' do - it "should not include rel=nofollow for internal links" do - expect(link_to("Home", root_path)).to eq("Home") + it 'should not include rel=nofollow for internal links' do + expect(link_to('Home', root_path)).to eq("Home") end - it "should include rel=nofollow for external links" do - expect(link_to("Example", "http://www.example.com")).to eq("Example") + it 'should include rel=nofollow for external links' do + expect(link_to('Example', 'http://www.example.com')).to eq("Example") end - it "should include re=nofollow for external links and honor existing html_options" do + it 'should include re=nofollow for external links and honor existing html_options' do expect( - link_to("Example", "http://www.example.com", class: "toggle", data: {toggle: "dropdown"}) + link_to('Example', 'http://www.example.com', class: 'toggle', data: {toggle: 'dropdown'}) ).to eq("Example") end - it "should include rel=nofollow for external links and preserver other rel values" do + it 'should include rel=nofollow for external links and preserver other rel values' do expect( - link_to("Example", "http://www.example.com", rel: "noreferrer") + link_to('Example', 'http://www.example.com', rel: 'noreferrer') ).to eq("Example") end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 87d26f98b4..c9bdeb43f6 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -14,7 +14,7 @@ # merge_requests_enabled :boolean default(TRUE), not null # wiki_enabled :boolean default(TRUE), not null # namespace_id :integer -# issues_tracker :string(255) default("gitlab"), not null +# issues_tracker :string(255) default('gitlab'), not null # issues_tracker_id :string(255) # snippets_enabled :boolean default(TRUE), not null # last_activity_at :datetime @@ -32,7 +32,7 @@ require 'spec_helper' describe Project do - describe "Associations" do + describe 'Associations' do it { should belong_to(:group) } it { should belong_to(:namespace) } it { should belong_to(:creator).class_name('User') } @@ -53,10 +53,10 @@ describe Project do it { should have_one(:pushover_service).dependent(:destroy) } end - describe "Mass assignment" do + describe 'Mass assignment' do end - describe "Validation" do + describe 'Validation' do let!(:project) { create(:project) } it { should validate_presence_of(:name) } @@ -71,7 +71,7 @@ describe Project do it { should ensure_length_of(:issues_tracker_id).is_within(0..255) } it { should validate_presence_of(:namespace) } - it "should not allow new projects beyond user limits" do + it 'should not allow new projects beyond user limits' do project2 = build(:project) project2.stub(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object) project2.should_not be_valid @@ -79,7 +79,7 @@ describe Project do end end - describe "Respond to" do + describe 'Respond to' do it { should respond_to(:url_to_repo) } it { should respond_to(:repo_exists?) } it { should respond_to(:satellite) } @@ -90,27 +90,27 @@ describe Project do it { should respond_to(:path_with_namespace) } end - it "should return valid url to repo" do - project = Project.new(path: "somewhere") - project.url_to_repo.should == Gitlab.config.gitlab_shell.ssh_path_prefix + "somewhere.git" + it 'should return valid url to repo' do + project = Project.new(path: 'somewhere') + project.url_to_repo.should == Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git' end - it "returns the full web URL for this repo" do - project = Project.new(path: "somewhere") + it 'returns the full web URL for this repo' do + project = Project.new(path: 'somewhere') project.web_url.should == "#{Gitlab.config.gitlab.url}/somewhere" end - it "returns the web URL without the protocol for this repo" do - project = Project.new(path: "somewhere") - project.web_url_without_protocol.should == "#{Gitlab.config.gitlab.url.split("://")[1]}/somewhere" + it 'returns the web URL without the protocol for this repo' do + project = Project.new(path: 'somewhere') + project.web_url_without_protocol.should == "#{Gitlab.config.gitlab.url.split('://')[1]}/somewhere" end - describe "last_activity methods" do + describe 'last_activity methods' do let(:project) { create(:project) } let(:last_event) { double(created_at: Time.now) } - describe "last_activity" do - it "should alias last_activity to last_event" do + describe 'last_activity' do + it 'should alias last_activity to last_event' do project.stub(last_event: last_event) project.last_activity.should == last_event end @@ -135,13 +135,13 @@ describe Project do let(:prev_commit_id) { merge_request.commits.last.id } let(:commit_id) { merge_request.commits.first.id } - it "should close merge request if last commit from source branch was pushed to target branch" do + it 'should close merge request if last commit from source branch was pushed to target branch' do project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.target_branch}", key.user) merge_request.reload merge_request.merged?.should be_true end - it "should update merge request commits with new one if pushed to source branch" do + it 'should update merge request commits with new one if pushed to source branch' do project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.source_branch}", key.user) merge_request.reload merge_request.last_commit.id.should == commit_id @@ -167,14 +167,14 @@ describe Project do @project = create(:project, name: 'gitlabhq', namespace: @group) end - it { @project.to_param.should == "gitlab/gitlabhq" } + it { @project.to_param.should == 'gitlab/gitlabhq' } end end describe :repository do let(:project) { create(:project) } - it "should return valid repo" do + it 'should return valid repo' do project.repository.should be_kind_of(Repository) end end @@ -185,15 +185,15 @@ describe Project do let(:not_existed_issue) { create(:issue) } let(:ext_project) { create(:redmine_project) } - it "should be true or if used internal tracker and issue exists" do + it 'should be true or if used internal tracker and issue exists' do project.issue_exists?(existed_issue.iid).should be_true end - it "should be false or if used internal tracker and issue not exists" do + it 'should be false or if used internal tracker and issue not exists' do project.issue_exists?(not_existed_issue.iid).should be_false end - it "should always be true if used other tracker" do + it 'should always be true if used other tracker' do ext_project.issue_exists?(rand(100)).should be_true end end @@ -202,11 +202,11 @@ describe Project do let(:project) { create(:project) } let(:ext_project) { create(:redmine_project) } - it "should be true if used internal tracker" do + it 'should be true if used internal tracker' do project.used_default_issues_tracker?.should be_true end - it "should be false if used other tracker" do + it 'should be false if used other tracker' do ext_project.used_default_issues_tracker?.should be_false end end @@ -215,15 +215,15 @@ describe Project do let(:project) { create(:project) } let(:ext_project) { create(:redmine_project) } - it "should be true for projects with external issues tracker if issues enabled" do + it 'should be true for projects with external issues tracker if issues enabled' do ext_project.can_have_issues_tracker_id?.should be_true end - it "should be false for projects with internal issue tracker if issues enabled" do + it 'should be false for projects with internal issue tracker if issues enabled' do project.can_have_issues_tracker_id?.should be_false end - it "should be always false if issues disabled" do + it 'should be always false if issues disabled' do project.issues_enabled = false ext_project.issues_enabled = false diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 67705c6cb4..8191d1fb9c 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -12,43 +12,43 @@ require 'spec_helper' # Examples # # # Default behavior -# it_behaves_like "RESTful project resources" do +# it_behaves_like 'RESTful project resources' do # let(:controller) { 'issues' } # end # # # Customizing actions -# it_behaves_like "RESTful project resources" do +# it_behaves_like 'RESTful project resources' do # let(:actions) { [:index] } # let(:controller) { 'issues' } # end -shared_examples "RESTful project resources" do +shared_examples 'RESTful project resources' do let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] } - it "to #index" do + it 'to #index' do get("/gitlab/gitlabhq/#{controller}").should route_to("projects/#{controller}#index", project_id: 'gitlab/gitlabhq') if actions.include?(:index) end - it "to #create" do + it 'to #create' do post("/gitlab/gitlabhq/#{controller}").should route_to("projects/#{controller}#create", project_id: 'gitlab/gitlabhq') if actions.include?(:create) end - it "to #new" do + it 'to #new' do get("/gitlab/gitlabhq/#{controller}/new").should route_to("projects/#{controller}#new", project_id: 'gitlab/gitlabhq') if actions.include?(:new) end - it "to #edit" do + it 'to #edit' do get("/gitlab/gitlabhq/#{controller}/1/edit").should route_to("projects/#{controller}#edit", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:edit) end - it "to #show" do + it 'to #show' do get("/gitlab/gitlabhq/#{controller}/1").should route_to("projects/#{controller}#show", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:show) end - it "to #update" do + it 'to #update' do put("/gitlab/gitlabhq/#{controller}/1").should route_to("projects/#{controller}#update", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:update) end - it "to #destroy" do + it 'to #destroy' do delete("/gitlab/gitlabhq/#{controller}/1").should route_to("projects/#{controller}#destroy", project_id: 'gitlab/gitlabhq', id: '1') if actions.include?(:destroy) end end @@ -61,33 +61,33 @@ end # PUT /:id(.:format) projects#update # DELETE /:id(.:format) projects#destroy # markdown_preview_project GET /:id/markdown_preview(.:format) projects#markdown_preview -describe ProjectsController, "routing" do - it "to #create" do - post("/projects").should route_to('projects#create') +describe ProjectsController, 'routing' do + it 'to #create' do + post('/projects').should route_to('projects#create') end - it "to #new" do - get("/projects/new").should route_to('projects#new') + it 'to #new' do + get('/projects/new').should route_to('projects#new') end - it "to #edit" do - get("/gitlab/gitlabhq/edit").should route_to('projects#edit', id: 'gitlab/gitlabhq') + it 'to #edit' do + get('/gitlab/gitlabhq/edit').should route_to('projects#edit', id: 'gitlab/gitlabhq') end - it "to #autocomplete_sources" do - get('/gitlab/gitlabhq/autocomplete_sources').should route_to('projects#autocomplete_sources', id: "gitlab/gitlabhq") + it 'to #autocomplete_sources' do + get('/gitlab/gitlabhq/autocomplete_sources').should route_to('projects#autocomplete_sources', id: 'gitlab/gitlabhq') end - it "to #show" do - get("/gitlab/gitlabhq").should route_to('projects#show', id: 'gitlab/gitlabhq') + it 'to #show' do + get('/gitlab/gitlabhq').should route_to('projects#show', id: 'gitlab/gitlabhq') end - it "to #update" do - put("/gitlab/gitlabhq").should route_to('projects#update', id: 'gitlab/gitlabhq') + it 'to #update' do + put('/gitlab/gitlabhq').should route_to('projects#update', id: 'gitlab/gitlabhq') end - it "to #destroy" do - delete("/gitlab/gitlabhq").should route_to('projects#destroy', id: 'gitlab/gitlabhq') + it 'to #destroy' do + delete('/gitlab/gitlabhq').should route_to('projects#destroy', id: 'gitlab/gitlabhq') end it 'to #markdown_preview' do @@ -103,16 +103,16 @@ end # edit_project_wiki GET /:project_id/wikis/:id/edit(.:format) projects/wikis#edit # project_wiki GET /:project_id/wikis/:id(.:format) projects/wikis#show # DELETE /:project_id/wikis/:id(.:format) projects/wikis#destroy -describe Projects::WikisController, "routing" do - it "to #pages" do - get("/gitlab/gitlabhq/wikis/pages").should route_to('projects/wikis#pages', project_id: 'gitlab/gitlabhq') +describe Projects::WikisController, 'routing' do + it 'to #pages' do + get('/gitlab/gitlabhq/wikis/pages').should route_to('projects/wikis#pages', project_id: 'gitlab/gitlabhq') end - it "to #history" do - get("/gitlab/gitlabhq/wikis/1/history").should route_to('projects/wikis#history', project_id: 'gitlab/gitlabhq', id: '1') + it 'to #history' do + get('/gitlab/gitlabhq/wikis/1/history').should route_to('projects/wikis#history', project_id: 'gitlab/gitlabhq', id: '1') end - it_behaves_like "RESTful project resources" do + it_behaves_like 'RESTful project resources' do let(:actions) { [:create, :edit, :show, :destroy] } let(:controller) { 'wikis' } end @@ -122,45 +122,45 @@ end # tags_project_repository GET /:project_id/repository/tags(.:format) projects/repositories#tags # archive_project_repository GET /:project_id/repository/archive(.:format) projects/repositories#archive # edit_project_repository GET /:project_id/repository/edit(.:format) projects/repositories#edit -describe Projects::RepositoriesController, "routing" do - it "to #archive" do - get("/gitlab/gitlabhq/repository/archive").should route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq') +describe Projects::RepositoriesController, 'routing' do + it 'to #archive' do + get('/gitlab/gitlabhq/repository/archive').should route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq') end - it "to #archive format:zip" do - get("/gitlab/gitlabhq/repository/archive.zip").should route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq', format: 'zip') + it 'to #archive format:zip' do + get('/gitlab/gitlabhq/repository/archive.zip').should route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq', format: 'zip') end - it "to #archive format:tar.bz2" do - get("/gitlab/gitlabhq/repository/archive.tar.bz2").should route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq', format: 'tar.bz2') + it 'to #archive format:tar.bz2' do + get('/gitlab/gitlabhq/repository/archive.tar.bz2').should route_to('projects/repositories#archive', project_id: 'gitlab/gitlabhq', format: 'tar.bz2') end - it "to #show" do - get("/gitlab/gitlabhq/repository").should route_to('projects/repositories#show', project_id: 'gitlab/gitlabhq') + it 'to #show' do + get('/gitlab/gitlabhq/repository').should route_to('projects/repositories#show', project_id: 'gitlab/gitlabhq') end end -describe Projects::BranchesController, "routing" do - it "to #branches" do - get("/gitlab/gitlabhq/branches").should route_to('projects/branches#index', project_id: 'gitlab/gitlabhq') - delete("/gitlab/gitlabhq/branches/feature%2345").should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45') - delete("/gitlab/gitlabhq/branches/feature%2B45").should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45') - delete("/gitlab/gitlabhq/branches/feature@45").should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45') - delete("/gitlab/gitlabhq/branches/feature%2345/foo/bar/baz").should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45/foo/bar/baz') - delete("/gitlab/gitlabhq/branches/feature%2B45/foo/bar/baz").should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45/foo/bar/baz') - delete("/gitlab/gitlabhq/branches/feature@45/foo/bar/baz").should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45/foo/bar/baz') +describe Projects::BranchesController, 'routing' do + it 'to #branches' do + get('/gitlab/gitlabhq/branches').should route_to('projects/branches#index', project_id: 'gitlab/gitlabhq') + delete('/gitlab/gitlabhq/branches/feature%2345').should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45') + delete('/gitlab/gitlabhq/branches/feature%2B45').should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45') + delete('/gitlab/gitlabhq/branches/feature@45').should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45') + delete('/gitlab/gitlabhq/branches/feature%2345/foo/bar/baz').should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45/foo/bar/baz') + delete('/gitlab/gitlabhq/branches/feature%2B45/foo/bar/baz').should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45/foo/bar/baz') + delete('/gitlab/gitlabhq/branches/feature@45/foo/bar/baz').should route_to('projects/branches#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45/foo/bar/baz') end end -describe Projects::TagsController, "routing" do - it "to #tags" do - get("/gitlab/gitlabhq/tags").should route_to('projects/tags#index', project_id: 'gitlab/gitlabhq') - delete("/gitlab/gitlabhq/tags/feature%2345").should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45') - delete("/gitlab/gitlabhq/tags/feature%2B45").should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45') - delete("/gitlab/gitlabhq/tags/feature@45").should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45') - delete("/gitlab/gitlabhq/tags/feature%2345/foo/bar/baz").should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45/foo/bar/baz') - delete("/gitlab/gitlabhq/tags/feature%2B45/foo/bar/baz").should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45/foo/bar/baz') - delete("/gitlab/gitlabhq/tags/feature@45/foo/bar/baz").should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45/foo/bar/baz') +describe Projects::TagsController, 'routing' do + it 'to #tags' do + get('/gitlab/gitlabhq/tags').should route_to('projects/tags#index', project_id: 'gitlab/gitlabhq') + delete('/gitlab/gitlabhq/tags/feature%2345').should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45') + delete('/gitlab/gitlabhq/tags/feature%2B45').should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45') + delete('/gitlab/gitlabhq/tags/feature@45').should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45') + delete('/gitlab/gitlabhq/tags/feature%2345/foo/bar/baz').should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature#45/foo/bar/baz') + delete('/gitlab/gitlabhq/tags/feature%2B45/foo/bar/baz').should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature+45/foo/bar/baz') + delete('/gitlab/gitlabhq/tags/feature@45/foo/bar/baz').should route_to('projects/tags#destroy', project_id: 'gitlab/gitlabhq', id: 'feature@45/foo/bar/baz') end end @@ -172,8 +172,8 @@ end # project_deploy_key GET /:project_id/deploy_keys/:id(.:format) deploy_keys#show # PUT /:project_id/deploy_keys/:id(.:format) deploy_keys#update # DELETE /:project_id/deploy_keys/:id(.:format) deploy_keys#destroy -describe Projects::DeployKeysController, "routing" do - it_behaves_like "RESTful project resources" do +describe Projects::DeployKeysController, 'routing' do + it_behaves_like 'RESTful project resources' do let(:controller) { 'deploy_keys' } end end @@ -181,8 +181,8 @@ end # project_protected_branches GET /:project_id/protected_branches(.:format) protected_branches#index # POST /:project_id/protected_branches(.:format) protected_branches#create # project_protected_branch DELETE /:project_id/protected_branches/:id(.:format) protected_branches#destroy -describe Projects::ProtectedBranchesController, "routing" do - it_behaves_like "RESTful project resources" do +describe Projects::ProtectedBranchesController, 'routing' do + it_behaves_like 'RESTful project resources' do let(:actions) { [:index, :create, :destroy] } let(:controller) { 'protected_branches' } end @@ -191,21 +191,21 @@ end # switch_project_refs GET /:project_id/refs/switch(.:format) refs#switch # logs_tree_project_ref GET /:project_id/refs/:id/logs_tree(.:format) refs#logs_tree # logs_file_project_ref GET /:project_id/refs/:id/logs_tree/:path(.:format) refs#logs_tree -describe Projects::RefsController, "routing" do - it "to #switch" do - get("/gitlab/gitlabhq/refs/switch").should route_to('projects/refs#switch', project_id: 'gitlab/gitlabhq') +describe Projects::RefsController, 'routing' do + it 'to #switch' do + get('/gitlab/gitlabhq/refs/switch').should route_to('projects/refs#switch', project_id: 'gitlab/gitlabhq') end - it "to #logs_tree" do - get("/gitlab/gitlabhq/refs/stable/logs_tree").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable') - get("/gitlab/gitlabhq/refs/feature%2345/logs_tree").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature#45') - get("/gitlab/gitlabhq/refs/feature%2B45/logs_tree").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature+45') - get("/gitlab/gitlabhq/refs/feature@45/logs_tree").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature@45') - get("/gitlab/gitlabhq/refs/stable/logs_tree/foo/bar/baz").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable', path: 'foo/bar/baz') - get("/gitlab/gitlabhq/refs/feature%2345/logs_tree/foo/bar/baz").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature#45', path: 'foo/bar/baz') - get("/gitlab/gitlabhq/refs/feature%2B45/logs_tree/foo/bar/baz").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature+45', path: 'foo/bar/baz') - get("/gitlab/gitlabhq/refs/feature@45/logs_tree/foo/bar/baz").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature@45', path: 'foo/bar/baz') - get("/gitlab/gitlabhq/refs/stable/logs_tree/files.scss").should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable', path: 'files.scss') + it 'to #logs_tree' do + get('/gitlab/gitlabhq/refs/stable/logs_tree').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable') + get('/gitlab/gitlabhq/refs/feature%2345/logs_tree').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature#45') + get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature+45') + get('/gitlab/gitlabhq/refs/feature@45/logs_tree').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature@45') + get('/gitlab/gitlabhq/refs/stable/logs_tree/foo/bar/baz').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable', path: 'foo/bar/baz') + get('/gitlab/gitlabhq/refs/feature%2345/logs_tree/foo/bar/baz').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature#45', path: 'foo/bar/baz') + get('/gitlab/gitlabhq/refs/feature%2B45/logs_tree/foo/bar/baz').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature+45', path: 'foo/bar/baz') + get('/gitlab/gitlabhq/refs/feature@45/logs_tree/foo/bar/baz').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'feature@45', path: 'foo/bar/baz') + get('/gitlab/gitlabhq/refs/stable/logs_tree/files.scss').should route_to('projects/refs#logs_tree', project_id: 'gitlab/gitlabhq', id: 'stable', path: 'files.scss') end end @@ -221,36 +221,36 @@ end # project_merge_request GET /:project_id/merge_requests/:id(.:format) projects/merge_requests#show # PUT /:project_id/merge_requests/:id(.:format) projects/merge_requests#update # DELETE /:project_id/merge_requests/:id(.:format) projects/merge_requests#destroy -describe Projects::MergeRequestsController, "routing" do - it "to #diffs" do - get("/gitlab/gitlabhq/merge_requests/1/diffs").should route_to('projects/merge_requests#diffs', project_id: 'gitlab/gitlabhq', id: '1') +describe Projects::MergeRequestsController, 'routing' do + it 'to #diffs' do + get('/gitlab/gitlabhq/merge_requests/1/diffs').should route_to('projects/merge_requests#diffs', project_id: 'gitlab/gitlabhq', id: '1') end - it "to #automerge" do + it 'to #automerge' do post('/gitlab/gitlabhq/merge_requests/1/automerge').should route_to( 'projects/merge_requests#automerge', project_id: 'gitlab/gitlabhq', id: '1' ) end - it "to #automerge_check" do - get("/gitlab/gitlabhq/merge_requests/1/automerge_check").should route_to('projects/merge_requests#automerge_check', project_id: 'gitlab/gitlabhq', id: '1') + it 'to #automerge_check' do + get('/gitlab/gitlabhq/merge_requests/1/automerge_check').should route_to('projects/merge_requests#automerge_check', project_id: 'gitlab/gitlabhq', id: '1') end - it "to #branch_from" do - get("/gitlab/gitlabhq/merge_requests/branch_from").should route_to('projects/merge_requests#branch_from', project_id: 'gitlab/gitlabhq') + it 'to #branch_from' do + get('/gitlab/gitlabhq/merge_requests/branch_from').should route_to('projects/merge_requests#branch_from', project_id: 'gitlab/gitlabhq') end - it "to #branch_to" do - get("/gitlab/gitlabhq/merge_requests/branch_to").should route_to('projects/merge_requests#branch_to', project_id: 'gitlab/gitlabhq') + it 'to #branch_to' do + get('/gitlab/gitlabhq/merge_requests/branch_to').should route_to('projects/merge_requests#branch_to', project_id: 'gitlab/gitlabhq') end - it "to #show" do - get("/gitlab/gitlabhq/merge_requests/1.diff").should route_to('projects/merge_requests#show', project_id: 'gitlab/gitlabhq', id: '1', format: 'diff') - get("/gitlab/gitlabhq/merge_requests/1.patch").should route_to('projects/merge_requests#show', project_id: 'gitlab/gitlabhq', id: '1', format: 'patch') + it 'to #show' do + get('/gitlab/gitlabhq/merge_requests/1.diff').should route_to('projects/merge_requests#show', project_id: 'gitlab/gitlabhq', id: '1', format: 'diff') + get('/gitlab/gitlabhq/merge_requests/1.patch').should route_to('projects/merge_requests#show', project_id: 'gitlab/gitlabhq', id: '1', format: 'patch') end - it_behaves_like "RESTful project resources" do + it_behaves_like 'RESTful project resources' do let(:controller) { 'merge_requests' } let(:actions) { [:index, :create, :new, :edit, :show, :update] } end @@ -264,37 +264,37 @@ end # project_snippet GET /:project_id/snippets/:id(.:format) snippets#show # PUT /:project_id/snippets/:id(.:format) snippets#update # DELETE /:project_id/snippets/:id(.:format) snippets#destroy -describe SnippetsController, "routing" do - it "to #raw" do - get("/gitlab/gitlabhq/snippets/1/raw").should route_to('projects/snippets#raw', project_id: 'gitlab/gitlabhq', id: '1') +describe SnippetsController, 'routing' do + it 'to #raw' do + get('/gitlab/gitlabhq/snippets/1/raw').should route_to('projects/snippets#raw', project_id: 'gitlab/gitlabhq', id: '1') end - it "to #index" do - get("/gitlab/gitlabhq/snippets").should route_to("projects/snippets#index", project_id: 'gitlab/gitlabhq') + it 'to #index' do + get('/gitlab/gitlabhq/snippets').should route_to('projects/snippets#index', project_id: 'gitlab/gitlabhq') end - it "to #create" do - post("/gitlab/gitlabhq/snippets").should route_to("projects/snippets#create", project_id: 'gitlab/gitlabhq') + it 'to #create' do + post('/gitlab/gitlabhq/snippets').should route_to('projects/snippets#create', project_id: 'gitlab/gitlabhq') end - it "to #new" do - get("/gitlab/gitlabhq/snippets/new").should route_to("projects/snippets#new", project_id: 'gitlab/gitlabhq') + it 'to #new' do + get('/gitlab/gitlabhq/snippets/new').should route_to('projects/snippets#new', project_id: 'gitlab/gitlabhq') end - it "to #edit" do - get("/gitlab/gitlabhq/snippets/1/edit").should route_to("projects/snippets#edit", project_id: 'gitlab/gitlabhq', id: '1') + it 'to #edit' do + get('/gitlab/gitlabhq/snippets/1/edit').should route_to('projects/snippets#edit', project_id: 'gitlab/gitlabhq', id: '1') end - it "to #show" do - get("/gitlab/gitlabhq/snippets/1").should route_to("projects/snippets#show", project_id: 'gitlab/gitlabhq', id: '1') + it 'to #show' do + get('/gitlab/gitlabhq/snippets/1').should route_to('projects/snippets#show', project_id: 'gitlab/gitlabhq', id: '1') end - it "to #update" do - put("/gitlab/gitlabhq/snippets/1").should route_to("projects/snippets#update", project_id: 'gitlab/gitlabhq', id: '1') + it 'to #update' do + put('/gitlab/gitlabhq/snippets/1').should route_to('projects/snippets#update', project_id: 'gitlab/gitlabhq', id: '1') end - it "to #destroy" do - delete("/gitlab/gitlabhq/snippets/1").should route_to("projects/snippets#destroy", project_id: 'gitlab/gitlabhq', id: '1') + it 'to #destroy' do + delete('/gitlab/gitlabhq/snippets/1').should route_to('projects/snippets#destroy', project_id: 'gitlab/gitlabhq', id: '1') end end @@ -302,24 +302,24 @@ end # project_hooks GET /:project_id/hooks(.:format) hooks#index # POST /:project_id/hooks(.:format) hooks#create # project_hook DELETE /:project_id/hooks/:id(.:format) hooks#destroy -describe Projects::HooksController, "routing" do - it "to #test" do - get("/gitlab/gitlabhq/hooks/1/test").should route_to('projects/hooks#test', project_id: 'gitlab/gitlabhq', id: '1') +describe Projects::HooksController, 'routing' do + it 'to #test' do + get('/gitlab/gitlabhq/hooks/1/test').should route_to('projects/hooks#test', project_id: 'gitlab/gitlabhq', id: '1') end - it_behaves_like "RESTful project resources" do + it_behaves_like 'RESTful project resources' do let(:actions) { [:index, :create, :destroy] } let(:controller) { 'hooks' } end end # project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /[[:alnum:]]{6,40}/, project_id: /[^\/]+/} -describe Projects::CommitController, "routing" do - it "to #show" do - get("/gitlab/gitlabhq/commit/4246fb").should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb') - get("/gitlab/gitlabhq/commit/4246fb.diff").should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb', format: 'diff') - get("/gitlab/gitlabhq/commit/4246fb.patch").should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb', format: 'patch') - get("/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5").should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') +describe Projects::CommitController, 'routing' do + it 'to #show' do + get('/gitlab/gitlabhq/commit/4246fb').should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb') + get('/gitlab/gitlabhq/commit/4246fb.diff').should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb', format: 'diff') + get('/gitlab/gitlabhq/commit/4246fb.patch').should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fb', format: 'patch') + get('/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5').should route_to('projects/commit#show', project_id: 'gitlab/gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') end end @@ -327,14 +327,14 @@ end # project_commits GET /:project_id/commits(.:format) commits#index # POST /:project_id/commits(.:format) commits#create # project_commit GET /:project_id/commits/:id(.:format) commits#show -describe Projects::CommitsController, "routing" do - it_behaves_like "RESTful project resources" do +describe Projects::CommitsController, 'routing' do + it_behaves_like 'RESTful project resources' do let(:actions) { [:show] } let(:controller) { 'commits' } end - it "to #show" do - get("/gitlab/gitlabhq/commits/master.atom").should route_to('projects/commits#show', project_id: 'gitlab/gitlabhq', id: "master", format: "atom") + it 'to #show' do + get('/gitlab/gitlabhq/commits/master.atom').should route_to('projects/commits#show', project_id: 'gitlab/gitlabhq', id: 'master', format: 'atom') end end @@ -345,8 +345,8 @@ end # project_team_member GET /:project_id/team_members/:id(.:format) team_members#show # PUT /:project_id/team_members/:id(.:format) team_members#update # DELETE /:project_id/team_members/:id(.:format) team_members#destroy -describe Projects::TeamMembersController, "routing" do - it_behaves_like "RESTful project resources" do +describe Projects::TeamMembersController, 'routing' do + it_behaves_like 'RESTful project resources' do let(:actions) { [:new, :create, :update, :destroy] } let(:controller) { 'team_members' } end @@ -359,17 +359,17 @@ end # project_milestone GET /:project_id/milestones/:id(.:format) milestones#show # PUT /:project_id/milestones/:id(.:format) milestones#update # DELETE /:project_id/milestones/:id(.:format) milestones#destroy -describe Projects::MilestonesController, "routing" do - it_behaves_like "RESTful project resources" do +describe Projects::MilestonesController, 'routing' do + it_behaves_like 'RESTful project resources' do let(:controller) { 'milestones' } let(:actions) { [:index, :create, :new, :edit, :show, :update] } end end # project_labels GET /:project_id/labels(.:format) labels#index -describe Projects::LabelsController, "routing" do - it "to #index" do - get("/gitlab/gitlabhq/labels").should route_to('projects/labels#index', project_id: 'gitlab/gitlabhq') +describe Projects::LabelsController, 'routing' do + it 'to #index' do + get('/gitlab/gitlabhq/labels').should route_to('projects/labels#index', project_id: 'gitlab/gitlabhq') end end @@ -383,12 +383,12 @@ end # project_issue GET /:project_id/issues/:id(.:format) issues#show # PUT /:project_id/issues/:id(.:format) issues#update # DELETE /:project_id/issues/:id(.:format) issues#destroy -describe Projects::IssuesController, "routing" do - it "to #bulk_update" do - post("/gitlab/gitlabhq/issues/bulk_update").should route_to('projects/issues#bulk_update', project_id: 'gitlab/gitlabhq') +describe Projects::IssuesController, 'routing' do + it 'to #bulk_update' do + post('/gitlab/gitlabhq/issues/bulk_update').should route_to('projects/issues#bulk_update', project_id: 'gitlab/gitlabhq') end - it_behaves_like "RESTful project resources" do + it_behaves_like 'RESTful project resources' do let(:controller) { 'issues' } let(:actions) { [:index, :create, :new, :edit, :show, :update] } end @@ -397,36 +397,36 @@ end # project_notes GET /:project_id/notes(.:format) notes#index # POST /:project_id/notes(.:format) notes#create # project_note DELETE /:project_id/notes/:id(.:format) notes#destroy -describe Projects::NotesController, "routing" do - it_behaves_like "RESTful project resources" do +describe Projects::NotesController, 'routing' do + it_behaves_like 'RESTful project resources' do let(:actions) { [:index, :create, :destroy] } let(:controller) { 'notes' } end end # project_blame GET /:project_id/blame/:id(.:format) blame#show {id: /.+/, project_id: /[^\/]+/} -describe Projects::BlameController, "routing" do - it "to #show" do - get("/gitlab/gitlabhq/blame/master/app/models/project.rb").should route_to('projects/blame#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') - get("/gitlab/gitlabhq/blame/master/files.scss").should route_to('projects/blame#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') +describe Projects::BlameController, 'routing' do + it 'to #show' do + get('/gitlab/gitlabhq/blame/master/app/models/project.rb').should route_to('projects/blame#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') + get('/gitlab/gitlabhq/blame/master/files.scss').should route_to('projects/blame#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') end end # project_blob GET /:project_id/blob/:id(.:format) blob#show {id: /.+/, project_id: /[^\/]+/} -describe Projects::BlobController, "routing" do - it "to #show" do - get("/gitlab/gitlabhq/blob/master/app/models/project.rb").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') - get("/gitlab/gitlabhq/blob/master/app/models/compare.rb").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/compare.rb') - get("/gitlab/gitlabhq/blob/master/app/models/diff.js").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/diff.js') - get("/gitlab/gitlabhq/blob/master/files.scss").should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') +describe Projects::BlobController, 'routing' do + it 'to #show' do + get('/gitlab/gitlabhq/blob/master/app/models/project.rb').should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') + get('/gitlab/gitlabhq/blob/master/app/models/compare.rb').should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/compare.rb') + get('/gitlab/gitlabhq/blob/master/app/models/diff.js').should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/diff.js') + get('/gitlab/gitlabhq/blob/master/files.scss').should route_to('projects/blob#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') end end # project_tree GET /:project_id/tree/:id(.:format) tree#show {id: /.+/, project_id: /[^\/]+/} -describe Projects::TreeController, "routing" do - it "to #show" do - get("/gitlab/gitlabhq/tree/master/app/models/project.rb").should route_to('projects/tree#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') - get("/gitlab/gitlabhq/tree/master/files.scss").should route_to('projects/tree#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') +describe Projects::TreeController, 'routing' do + it 'to #show' do + get('/gitlab/gitlabhq/tree/master/app/models/project.rb').should route_to('projects/tree#show', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb') + get('/gitlab/gitlabhq/tree/master/files.scss').should route_to('projects/tree#show', project_id: 'gitlab/gitlabhq', id: 'master/files.scss') end end @@ -453,42 +453,43 @@ end # project_compare_index GET /:project_id/compare(.:format) compare#index {id: /[^\/]+/, project_id: /[^\/]+/} # POST /:project_id/compare(.:format) compare#create {id: /[^\/]+/, project_id: /[^\/]+/} # project_compare /:project_id/compare/:from...:to(.:format) compare#show {from: /.+/, to: /.+/, id: /[^\/]+/, project_id: /[^\/]+/} -describe Projects::CompareController, "routing" do - it "to #index" do - get("/gitlab/gitlabhq/compare").should route_to('projects/compare#index', project_id: 'gitlab/gitlabhq') +describe Projects::CompareController, 'routing' do + it 'to #index' do + get('/gitlab/gitlabhq/compare').should route_to('projects/compare#index', project_id: 'gitlab/gitlabhq') end - it "to #compare" do - post("/gitlab/gitlabhq/compare").should route_to('projects/compare#create', project_id: 'gitlab/gitlabhq') + it 'to #compare' do + post('/gitlab/gitlabhq/compare').should route_to('projects/compare#create', project_id: 'gitlab/gitlabhq') end - it "to #show" do - get("/gitlab/gitlabhq/compare/master...stable").should route_to('projects/compare#show', project_id: 'gitlab/gitlabhq', from: 'master', to: 'stable') - get("/gitlab/gitlabhq/compare/issue/1234...stable").should route_to('projects/compare#show', project_id: 'gitlab/gitlabhq', from: 'issue/1234', to: 'stable') + it 'to #show' do + get('/gitlab/gitlabhq/compare/master...stable').should route_to('projects/compare#show', project_id: 'gitlab/gitlabhq', from: 'master', to: 'stable') + get('/gitlab/gitlabhq/compare/issue/1234...stable').should route_to('projects/compare#show', project_id: 'gitlab/gitlabhq', from: 'issue/1234', to: 'stable') end end -describe Projects::NetworkController, "routing" do - it "to #show" do - get("/gitlab/gitlabhq/network/master").should route_to('projects/network#show', project_id: 'gitlab/gitlabhq', id: 'master') - get("/gitlab/gitlabhq/network/master.json").should route_to('projects/network#show', project_id: 'gitlab/gitlabhq', id: 'master', format: "json") +describe Projects::NetworkController, 'routing' do + it 'to #show' do + get('/gitlab/gitlabhq/network/master').should route_to('projects/network#show', project_id: 'gitlab/gitlabhq', id: 'master') + get('/gitlab/gitlabhq/network/master.json').should route_to('projects/network#show', project_id: 'gitlab/gitlabhq', id: 'master', format: 'json') end end -describe Projects::GraphsController, "routing" do - it "to #show" do - get("/gitlab/gitlabhq/graphs/master").should route_to('projects/graphs#show', project_id: 'gitlab/gitlabhq', id: 'master') +describe Projects::GraphsController, 'routing' do + it 'to #show' do + get('/gitlab/gitlabhq/graphs/master').should route_to('projects/graphs#show', project_id: 'gitlab/gitlabhq', id: 'master') end end -describe Projects::ForksController, "routing" do - it "to #new" do - get("/gitlab/gitlabhq/fork/new").should route_to("projects/forks#new", project_id: 'gitlab/gitlabhq') +describe Projects::ForksController, 'routing' do + it 'to #new' do + get('/gitlab/gitlabhq/fork/new').should route_to('projects/forks#new', project_id: 'gitlab/gitlabhq') end - it "to #create" do - post("/gitlab/gitlabhq/fork").should route_to("projects/forks#create", project_id: 'gitlab/gitlabhq') + it 'to #create' do + post('/gitlab/gitlabhq/fork').should route_to('projects/forks#create', project_id: 'gitlab/gitlabhq') end +end # project_avatar DELETE /project/avatar(.:format) projects/avatars#destroy describe Projects::AvatarsController, 'routing' do From 517cc92c65f5b8127d40f9c918ea986483798e90 Mon Sep 17 00:00:00 2001 From: Yatish Mehta Date: Sat, 24 Jan 2015 14:25:16 -0500 Subject: [PATCH 059/124] Fixes typo in config.rb --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index ef3c5aedfc..abb77094ab 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -61,7 +61,7 @@ Gitlab::Application.routes.draw do end # - # Explroe area + # Explore area # namespace :explore do resources :projects, only: [:index] do From b92449c73e3b80a85144de08e0062c74cb37e80d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 24 Jan 2015 22:03:30 -0800 Subject: [PATCH 060/124] Predefine colors for project identicons --- app/assets/stylesheets/generic/avatar.scss | 4 ++-- app/assets/stylesheets/sections/dashboard.scss | 2 +- app/helpers/application_helper.rb | 17 +++++++++++++---- app/views/dashboard/_project.html.haml | 4 ++-- app/views/groups/_projects.html.haml | 4 ++-- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/generic/avatar.scss b/app/assets/stylesheets/generic/avatar.scss index f04848ae6d..b688620673 100644 --- a/app/assets/stylesheets/generic/avatar.scss +++ b/app/assets/stylesheets/generic/avatar.scss @@ -29,10 +29,10 @@ vertical-align: top; &.s16 { font-size: 12px; line-height: 1.33; } - &.s24 { font-size: 18px; line-height: 1.33; } + &.s24 { font-size: 14px; line-height: 1.8; } &.s26 { font-size: 20px; line-height: 1.33; } &.s32 { font-size: 24px; line-height: 1.33; } &.s60 { font-size: 45px; line-height: 1.33; } &.s90 { font-size: 68px; line-height: 1.33; } &.s160 { font-size: 120px; line-height: 1.33; } -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/sections/dashboard.scss b/app/assets/stylesheets/sections/dashboard.scss index 3135056db5..00795f990b 100644 --- a/app/assets/stylesheets/sections/dashboard.scss +++ b/app/assets/stylesheets/sections/dashboard.scss @@ -100,7 +100,7 @@ } .dash-project-access-icon { float: left; - margin-right: 3px; + margin-right: 5px; width: 16px; } diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 32fd0ed7bc..f253ae9130 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -64,13 +64,22 @@ module ApplicationHelper end def project_identicon(project, options = {}) + allowed_colors = { + red: 'FFEBEE', + purple: 'F3E5F5', + indigo: 'E8EAF6', + blue: 'E3F2FD', + teal: 'E0F2F1', + orange: 'FBE9E7', + gray: 'EEEEEE' + } + options[:class] ||= '' options[:class] << ' identicon' - bg_color = Digest::MD5.hexdigest(project.name)[0, 6] - brightness = bg_color[0, 2].hex + bg_color[2, 2].hex + bg_color[4, 2].hex - text_color = (brightness > 375) ? '#000' : '#fff' + bg_key = project.id % 7 + content_tag(:div, class: options[:class], - style: "background-color: ##{ bg_color }; color: #{ text_color }") do + style: "background-color: ##{ allowed_colors.values[bg_key] }; color: #555") do project.name[0, 1].upcase end end diff --git a/app/views/dashboard/_project.html.haml b/app/views/dashboard/_project.html.haml index 7f19fb5a81..76b95264fd 100644 --- a/app/views/dashboard/_project.html.haml +++ b/app/views/dashboard/_project.html.haml @@ -1,8 +1,8 @@ = link_to project_path(project), class: dom_class(project) do - .dash-project-avatar - = project_icon(project.to_param, alt: '', class: 'avatar s24') .dash-project-access-icon = visibility_level_icon(project.visibility_level) + .dash-project-avatar + = project_icon(project.to_param, alt: '', class: 'avatar s24') %span.str-truncated %span.namespace-name - if project.namespace diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 2716ebf326..34221595fd 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -12,10 +12,10 @@ - projects.each do |project| %li.project-row = link_to project_path(project), class: dom_class(project) do - .dash-project-avatar - = project_icon(project.to_param, alt: '', class: 'avatar s24') .dash-project-access-icon = visibility_level_icon(project.visibility_level) + .dash-project-avatar + = project_icon(project.to_param, alt: '', class: 'avatar s24') %span.str-truncated %span.project-name = project.name From 3588a07b8379c98029598cac8050a1f90fd9c354 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 24 Jan 2015 23:08:52 -0800 Subject: [PATCH 061/124] Remove default project icon --- app/assets/images/no_project_icon.png | Bin 3387 -> 0 bytes app/helpers/application_helper.rb | 2 -- app/views/projects/edit.html.haml | 2 -- 3 files changed, 4 deletions(-) delete mode 100644 app/assets/images/no_project_icon.png diff --git a/app/assets/images/no_project_icon.png b/app/assets/images/no_project_icon.png deleted file mode 100644 index 8e9529c67ec3a7167c59f2c52bdb4b009b9803cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3387 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 Date: Sat, 24 Jan 2015 23:35:02 -0800 Subject: [PATCH 062/124] Fix avatar margin for project home page --- app/assets/stylesheets/sections/projects.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/sections/projects.scss b/app/assets/stylesheets/sections/projects.scss index 93c0c2bc51..70adc21a7a 100644 --- a/app/assets/stylesheets/sections/projects.scss +++ b/app/assets/stylesheets/sections/projects.scss @@ -36,6 +36,10 @@ float: left; color: #666; font-size: 16px; + + .avatar { + margin-top: -5px; + } } .star-fork-buttons { From a4dad7085850aa62134ed23b90f3a045c3569663 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sat, 24 Jan 2015 23:59:03 -0800 Subject: [PATCH 063/124] Fix project name truncation for dashboard --- app/assets/stylesheets/sections/dashboard.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/sections/dashboard.scss b/app/assets/stylesheets/sections/dashboard.scss index 00795f990b..90010781af 100644 --- a/app/assets/stylesheets/sections/dashboard.scss +++ b/app/assets/stylesheets/sections/dashboard.scss @@ -112,3 +112,7 @@ color: #FFF; } } + +.dash-list .str-truncated { + max-width: 72%; +} From ce9686e3f56658f2fb8df8ad6e1335e338875df9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 25 Jan 2015 19:59:04 -0800 Subject: [PATCH 064/124] Include issue/mr participants in list of recipients for close/reopen emails --- app/services/notification_service.rb | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 72c9149378..87366b6572 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -331,7 +331,14 @@ class NotificationService end def close_resource_email(target, project, current_user, method) - recipients = reject_muted_users([target.author, target.assignee], project) + participants = + if target.respond_to?(:participants) + target.participants + else + [target.author, target.assignee] + end + + recipients = reject_muted_users(participants, project) recipients = reject_mention_users(recipients, project) recipients = recipients.concat(project_watchers(project)).uniq recipients.delete(current_user) @@ -362,7 +369,14 @@ class NotificationService end def reopen_resource_email(target, project, current_user, method, status) - recipients = reject_muted_users([target.author, target.assignee], project) + participants = + if target.respond_to?(:participants) + target.participants + else + [target.author, target.assignee] + end + + recipients = reject_muted_users(participants, project) recipients = reject_mention_users(recipients, project) recipients = recipients.concat(project_watchers(project)).uniq recipients.delete(current_user) From 615488150bd176088d5b37a2441fd1f11396edf7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 25 Jan 2015 20:01:32 -0800 Subject: [PATCH 065/124] Update CHANGELOG with notify participants change --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index dd9b13ceac..6af250cb39 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,7 +3,7 @@ Note: The upcoming release contains empty lines to reduce the number of merge co v 7.8.0 - Replace highlight.js with rouge-fork rugments (Stefan Tatschner) - Make project search case insensitive (Hannes Rosenögger) - - + - Include issue/mr participants in list of recipients for close/reopen emails - Expose description in groups API - - From 2b02852507466c8cd7dc9a7db7a7e0dd4c7b5183 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 25 Jan 2015 20:31:02 -0800 Subject: [PATCH 066/124] Add issue/mr participants to reasign events Also refactor NotificationService a bit --- CHANGELOG | 2 +- app/services/notification_service.rb | 52 +++++++--------------- spec/services/notification_service_spec.rb | 7 ++- 3 files changed, 22 insertions(+), 39 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6af250cb39..72ca2b529d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,7 +3,7 @@ Note: The upcoming release contains empty lines to reduce the number of merge co v 7.8.0 - Replace highlight.js with rouge-fork rugments (Stefan Tatschner) - Make project search case insensitive (Hannes Rosenögger) - - Include issue/mr participants in list of recipients for close/reopen emails + - Include issue/mr participants in list of recipients for reassign/close/reopen emails - Expose description in groups API - - diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 87366b6572..2fc63b9f4b 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -314,15 +314,7 @@ class NotificationService end def new_resource_email(target, project, method) - if target.respond_to?(:participants) - recipients = target.participants - else - recipients = [] - end - - recipients = reject_muted_users(recipients, project) - recipients = reject_mention_users(recipients, project) - recipients = recipients.concat(project_watchers(project)).uniq + recipients = build_recipients(target, project) recipients.delete(target.author) recipients.each do |recipient| @@ -331,16 +323,7 @@ class NotificationService end def close_resource_email(target, project, current_user, method) - participants = - if target.respond_to?(:participants) - target.participants - else - [target.author, target.assignee] - end - - recipients = reject_muted_users(participants, project) - recipients = reject_mention_users(recipients, project) - recipients = recipients.concat(project_watchers(project)).uniq + recipients = build_recipients(target, project) recipients.delete(current_user) recipients.each do |recipient| @@ -350,17 +333,7 @@ class NotificationService def reassign_resource_email(target, project, current_user, method) assignee_id_was = previous_record(target, "assignee_id") - - recipients = User.where(id: [target.assignee_id, assignee_id_was]) - - # Add watchers to email list - recipients = recipients.concat(project_watchers(project)) - - # reject users with disabled notifications - recipients = reject_muted_users(recipients, project) - recipients = reject_mention_users(recipients, project) - - # Reject me from recipients if I reassign an item + recipients = build_recipients(target, project) recipients.delete(current_user) recipients.each do |recipient| @@ -369,21 +342,26 @@ class NotificationService end def reopen_resource_email(target, project, current_user, method, status) - participants = + recipients = build_recipients(target, project) + recipients.delete(current_user) + + recipients.each do |recipient| + mailer.send(method, recipient.id, target.id, status, current_user.id) + end + end + + def build_recipients(target, project) + recipients = if target.respond_to?(:participants) target.participants else [target.author, target.assignee] end - recipients = reject_muted_users(participants, project) + recipients = reject_muted_users(recipients, project) recipients = reject_mention_users(recipients, project) recipients = recipients.concat(project_watchers(project)).uniq - recipients.delete(current_user) - - recipients.each do |recipient| - mailer.send(method, recipient.id, target.id, status, current_user.id) - end + recipients end def mailer diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index e305536f7e..2ba1e3372b 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -187,7 +187,7 @@ describe NotificationService do end describe 'Issues' do - let(:issue) { create :issue, assignee: create(:user) } + let(:issue) { create :issue, assignee: create(:user), description: 'cc @participant' } before do build_team(issue.project) @@ -197,6 +197,7 @@ describe NotificationService do it do should_email(issue.assignee_id) should_email(@u_watcher.id) + should_email(@u_participant_mentioned.id) should_not_email(@u_mentioned.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) @@ -222,6 +223,7 @@ describe NotificationService do it 'should email new assignee' do should_email(issue.assignee_id) should_email(@u_watcher.id) + should_email(@u_participant_mentioned.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) @@ -242,6 +244,7 @@ describe NotificationService do should_email(issue.assignee_id) should_email(issue.author_id) should_email(@u_watcher.id) + should_email(@u_participant_mentioned.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) @@ -262,6 +265,7 @@ describe NotificationService do should_email(issue.assignee_id) should_email(issue.author_id) should_email(@u_watcher.id) + should_email(@u_participant_mentioned.id) should_not_email(@u_participating.id) should_not_email(@u_disabled.id) @@ -404,6 +408,7 @@ describe NotificationService do def build_team(project) @u_watcher = create(:user, notification_level: Notification::N_WATCH) @u_participating = create(:user, notification_level: Notification::N_PARTICIPATING) + @u_participant_mentioned = create(:user, username: 'participant', notification_level: Notification::N_PARTICIPATING) @u_disabled = create(:user, notification_level: Notification::N_DISABLED) @u_mentioned = create(:user, username: 'mention', notification_level: Notification::N_MENTION) @u_committer = create(:user, username: 'committer') From 9d85ea3acff1c925118718996afc9daa39d679c7 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Sun, 25 Jan 2015 22:49:05 -0800 Subject: [PATCH 067/124] Organize event order execution when update issue or mr --- app/services/issues/close_service.rb | 2 +- app/services/issues/update_service.rb | 2 +- app/services/merge_requests/auto_merge_service.rb | 2 +- app/services/merge_requests/close_service.rb | 2 +- app/services/merge_requests/merge_service.rb | 2 +- app/services/merge_requests/reopen_service.rb | 2 +- app/services/merge_requests/update_service.rb | 2 +- spec/services/issues/update_service_spec.rb | 1 + spec/services/merge_requests/update_service_spec.rb | 4 +++- 9 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb index ffed13a12e..f670019cc6 100644 --- a/app/services/issues/close_service.rb +++ b/app/services/issues/close_service.rb @@ -2,9 +2,9 @@ module Issues class CloseService < Issues::BaseService def execute(issue, commit = nil) if issue.close - notification_service.close_issue(issue, current_user) event_service.close_issue(issue, current_user) create_note(issue, commit) + notification_service.close_issue(issue, current_user) execute_hooks(issue, 'close') end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 0ee9635ed9..83e413d724 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -23,8 +23,8 @@ module Issues end if issue.previous_changes.include?('assignee_id') - notification_service.reassigned_issue(issue, current_user) create_assignee_note(issue) + notification_service.reassigned_issue(issue, current_user) end issue.notice_added_references(issue.project, current_user) diff --git a/app/services/merge_requests/auto_merge_service.rb b/app/services/merge_requests/auto_merge_service.rb index b5d90a74e1..378b39bb9d 100644 --- a/app/services/merge_requests/auto_merge_service.rb +++ b/app/services/merge_requests/auto_merge_service.rb @@ -11,9 +11,9 @@ module MergeRequests if Gitlab::Satellite::MergeAction.new(current_user, merge_request).merge!(commit_message) merge_request.merge - notification_service.merge_mr(merge_request, current_user) create_merge_event(merge_request, current_user) create_note(merge_request) + notification_service.merge_mr(merge_request, current_user) execute_hooks(merge_request) true diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb index 4249a84f38..47454f9f0c 100644 --- a/app/services/merge_requests/close_service.rb +++ b/app/services/merge_requests/close_service.rb @@ -7,8 +7,8 @@ module MergeRequests if merge_request.close event_service.close_mr(merge_request, current_user) - notification_service.close_mr(merge_request, current_user) create_note(merge_request) + notification_service.close_mr(merge_request, current_user) execute_hooks(merge_request, 'close') end diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 1e1614028f..327ead4ff3 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -9,9 +9,9 @@ module MergeRequests def execute(merge_request, commit_message) merge_request.merge - notification_service.merge_mr(merge_request, current_user) create_merge_event(merge_request, current_user) create_note(merge_request) + notification_service.merge_mr(merge_request, current_user) execute_hooks(merge_request, 'merge') true diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb index a2a9c933f6..8279ad2001 100644 --- a/app/services/merge_requests/reopen_service.rb +++ b/app/services/merge_requests/reopen_service.rb @@ -3,8 +3,8 @@ module MergeRequests def execute(merge_request) if merge_request.reopen event_service.reopen_mr(merge_request, current_user) - notification_service.reopen_mr(merge_request, current_user) create_note(merge_request) + notification_service.reopen_mr(merge_request, current_user) execute_hooks(merge_request, 'reopen') merge_request.reload_code merge_request.mark_as_unchecked diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 56c8510e0a..10c401756e 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -33,8 +33,8 @@ module MergeRequests end if merge_request.previous_changes.include?('assignee_id') - notification_service.reassigned_merge_request(merge_request, current_user) create_assignee_note(merge_request) + notification_service.reassigned_merge_request(merge_request, current_user) end merge_request.notice_added_references(merge_request.project, current_user) diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 347560414e..3603057783 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -22,6 +22,7 @@ describe Issues::UpdateService do } @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + @issue.reload end it { @issue.should be_valid } diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index c8f40f48ba..0cd822bcda 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -21,12 +21,14 @@ describe MergeRequests::UpdateService do state_event: 'close' } end + let(:service) { MergeRequests::UpdateService.new(project, user, opts) } before do service.stub(:execute_hooks) @merge_request = service.execute(merge_request) + @merge_request.reload end it { @merge_request.should be_valid } @@ -46,7 +48,7 @@ describe MergeRequests::UpdateService do end it 'should create system note about merge_request reassign' do - note = @merge_request.notes.last + note = @merge_request.notes.reload.last note.note.should include "Reassigned to \@#{user2.username}" end end From e20dba0299c1395cc8862c44b0b3933266abe001 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 00:23:16 -0800 Subject: [PATCH 068/124] Redesign way how project avatar displayed on project page --- app/assets/stylesheets/sections/projects.scss | 22 +++++++++++++++---- app/views/projects/_home_panel.html.haml | 3 ++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/sections/projects.scss b/app/assets/stylesheets/sections/projects.scss index 70adc21a7a..0a7671e3fe 100644 --- a/app/assets/stylesheets/sections/projects.scss +++ b/app/assets/stylesheets/sections/projects.scss @@ -16,6 +16,8 @@ .project-home-panel { margin-bottom: 15px; + position: relative; + padding-left: 85px; &.empty-project { border-bottom: 0px; @@ -23,6 +25,22 @@ margin-bottom: 0px; } + .project-identicon-holder { + position: absolute; + left: 0; + + .avatar { + width: 70px; + height: 70px; + @include border-radius(0px); + } + + .identicon { + font-size: 45px; + line-height: 1.6; + } + } + .project-home-dropdown { margin-left: 10px; float: right; @@ -36,10 +54,6 @@ float: left; color: #666; font-size: 16px; - - .avatar { - margin-top: -5px; - } } .star-fork-buttons { diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 05910c6038..2ed49f83a7 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,8 +1,9 @@ - empty_repo = @project.empty_repo? .project-home-panel{:class => ("empty-project" if empty_repo)} + .project-identicon-holder + = project_icon(@project.to_param, alt: '', class: 'avatar') .project-home-row .project-home-desc - = project_icon(@project.to_param, alt: '', class: 'avatar s32') - if @project.description.present? = escaped_autolink(@project.description) - if can?(current_user, :admin_project, @project) From 65e700472b471242475eb9d9e3a340c6ce24615a Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 26 Jan 2015 11:39:32 -0800 Subject: [PATCH 069/124] Update the issue tracker attribute on issue tracker change. --- app/controllers/projects/services_controller.rb | 3 +++ app/helpers/issues_helper.rb | 6 +++--- app/models/project.rb | 17 ++++++++--------- lib/gitlab/markdown.rb | 2 +- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 15f47ed9c9..a2cb4ae1ae 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -17,6 +17,9 @@ class Projects::ServicesController < Projects::ApplicationController def update if @service.update_attributes(service_params) + if @service.activated? && @service.category == :issue_tracker + @project.update_attributes(issues_tracker: @service.to_param) + end redirect_to edit_project_service_path(@project, @service.to_param), notice: 'Successfully updated.' else diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index cfbbed842c..2bf430f914 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -16,7 +16,7 @@ module IssuesHelper def url_for_project_issues(project = @project) return '' if project.nil? - if project.using_issue_tracker? + if project.default_issues_tracker? project_issues_path(project) else project.external_issue_tracker.project_url @@ -26,7 +26,7 @@ module IssuesHelper def url_for_new_issue(project = @project) return '' if project.nil? - if project.using_issue_tracker? + if project.default_issues_tracker? url = new_project_issue_path project_id: project else project.external_issue_tracker.new_issue_url @@ -36,7 +36,7 @@ module IssuesHelper def url_for_issue(issue_iid, project = @project) return '' if project.nil? - if project.using_issue_tracker? + if project.default_issues_tracker? url = project_issue_url project_id: project, id: issue_iid else url = project.external_issue_tracker.issues_url diff --git a/app/models/project.rb b/app/models/project.rb index 20b9a5a34d..de31f14b98 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -308,11 +308,14 @@ class Project < ActiveRecord::Base end def default_issues_tracker? - self.issues_tracker == Project.issues_tracker.default_value - end - - def external_issues_tracker_enabled? - external_issues_trackers.any? + if external_issue_tracker + false + else + unless self.issues_tracker == Project.issues_tracker.default_value + self.update_attributes(issues_tracker: Project.issues_tracker.default_value) + end + true + end end def external_issues_trackers @@ -323,10 +326,6 @@ class Project < ActiveRecord::Base @external_issues_tracker ||= external_issues_trackers.select(&:activated?).first end - def using_issue_tracker? - default_issues_tracker? || !external_issues_tracker_enabled? - end - def can_have_issues_tracker_id? self.issues_enabled && !self.default_issues_tracker? end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 6ba7a0c18f..2f04133647 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -208,7 +208,7 @@ module Gitlab end def reference_issue(identifier, project = @project, prefix_text = nil) - if project.using_issue_tracker? + if project.default_issues_tracker? if project.issue_exists? identifier url = url_for_issue(identifier, project) title = title_for_issue(identifier, project) From 05b6bb4bf704c2a47d6b2717308ee2ad9b5eec81 Mon Sep 17 00:00:00 2001 From: Marco Wessel Date: Mon, 26 Jan 2015 21:25:13 +0100 Subject: [PATCH 070/124] Don't require omniauth to be enabled, to use github importer --- app/helpers/projects_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index de232ab4e2..f780a8ffc3 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -257,7 +257,7 @@ module ProjectsHelper end def github_import_enabled? - Gitlab.config.omniauth.enabled && enabled_oauth_providers.include?(:github) + enabled_oauth_providers.include?(:github) end end From 3e47ea5064f7e93cadb0ef347dfa27517552a4a0 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 13:31:20 -0800 Subject: [PATCH 071/124] Files::CreateService can now commit file to empty repository --- app/services/files/create_service.rb | 19 ++++++++++++------- lib/gitlab/satellite/files/new_file_action.rb | 12 +++++++++--- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index b90adeef00..2c457ef2ce 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -9,10 +9,6 @@ module Files return error("You are not allowed to create file in this branch") end - unless repository.branch_names.include?(ref) - return error("You can only create files if you are on top of a branch") - end - file_name = File.basename(path) file_path = path @@ -23,12 +19,21 @@ module Files ) end - blob = repository.blob_at_branch(ref, file_path) + if project.empty_repo? + # everything is ok because repo does not have a commits yet + else + unless repository.branch_names.include?(ref) + return error("You can only create files if you are on top of a branch") + end - if blob - return error("Your changes could not be committed, because file with such name exists") + blob = repository.blob_at_branch(ref, file_path) + + if blob + return error("Your changes could not be committed, because file with such name exists") + end end + new_file_action = Gitlab::Satellite::NewFileAction.new(current_user, project, ref, file_path) created_successfully = new_file_action.commit!( params[:content], diff --git a/lib/gitlab/satellite/files/new_file_action.rb b/lib/gitlab/satellite/files/new_file_action.rb index 15e9b7a6f7..c230239d39 100644 --- a/lib/gitlab/satellite/files/new_file_action.rb +++ b/lib/gitlab/satellite/files/new_file_action.rb @@ -14,7 +14,14 @@ module Gitlab prepare_satellite!(repo) # create target branch in satellite at the corresponding commit from bare repo - repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}") + current_ref = + if repo.commits.any? + repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}") + ref + else + # skip this step if we want to add first file to empty repo + Satellite::PARKING_BRANCH + end file_path_in_satellite = File.join(repo.working_dir, file_path) dir_name_in_satellite = File.dirname(file_path_in_satellite) @@ -38,10 +45,9 @@ module Gitlab # will raise CommandFailed when commit fails repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) - # push commit back to bare repo # will raise CommandFailed when push fails - repo.git.push({raise: true, timeout: true}, :origin, ref) + repo.git.push({raise: true, timeout: true}, :origin, "#{current_ref}:#{ref}") # everything worked true From 6a2384e0bc7d0b703c8f6af144c85af47eae6630 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 13:37:18 -0800 Subject: [PATCH 072/124] Make draft UI for creating new file in empty repository --- app/assets/stylesheets/generic/buttons.scss | 5 +++++ app/views/projects/empty.html.haml | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss index d098f1ecaa..3b36027506 100644 --- a/app/assets/stylesheets/generic/buttons.scss +++ b/app/assets/stylesheets/generic/buttons.scss @@ -173,6 +173,11 @@ margin-right: 0px; } } + + &.btn-lg { + font-size: 15px; + line-height: 1.4; + } } .btn-block { diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 2e46de6bfe..3a42fce43e 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -3,6 +3,17 @@ = render "home_panel" +.center.well + %h3 + The repository for this project is empty + %p.lead + You can + = link_to '#', class: 'btn btn-new btn-lg' do + add a file +  or push it via command line. + +%h4 + %strong Command line instructions %div.git-empty %fieldset %legend Git global setup From c916124178645412a554a6b8b39c05bbd42269c8 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 15:01:51 -0800 Subject: [PATCH 073/124] Explicitly set before_filter for ref-related controllers --- app/controllers/projects/blame_controller.rb | 2 +- app/controllers/projects/commits_controller.rb | 2 +- app/controllers/projects/network_controller.rb | 2 +- app/controllers/projects/raw_controller.rb | 2 +- app/controllers/projects/refs_controller.rb | 2 +- app/controllers/projects/tree_controller.rb | 9 +++++++-- lib/extracts_path.rb | 8 -------- 7 files changed, 12 insertions(+), 15 deletions(-) diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index 367d1295f3..106f21b83e 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -2,7 +2,7 @@ class Projects::BlameController < Projects::ApplicationController include ExtractsPath - # Authorize + before_filter :assign_ref_vars before_filter :authorize_download_code! before_filter :require_non_empty_project diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 9476b6c028..0a85c36a75 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -3,7 +3,7 @@ require "base64" class Projects::CommitsController < Projects::ApplicationController include ExtractsPath - # Authorize + before_filter :assign_ref_vars before_filter :authorize_download_code! before_filter :require_non_empty_project diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb index ada1aed0df..59f2a74536 100644 --- a/app/controllers/projects/network_controller.rb +++ b/app/controllers/projects/network_controller.rb @@ -2,7 +2,7 @@ class Projects::NetworkController < Projects::ApplicationController include ExtractsPath include ApplicationHelper - # Authorize + before_filter :assign_ref_vars before_filter :authorize_download_code! before_filter :require_non_empty_project diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index fdbc4c5a09..84888265dc 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -2,7 +2,7 @@ class Projects::RawController < Projects::ApplicationController include ExtractsPath - # Authorize + before_filter :assign_ref_vars before_filter :authorize_download_code! before_filter :require_non_empty_project diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 67665f5f60..cede0ebe0a 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -1,7 +1,7 @@ class Projects::RefsController < Projects::ApplicationController include ExtractsPath - # Authorize + before_filter :assign_ref_vars before_filter :authorize_download_code! before_filter :require_non_empty_project diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index 4d033b3684..5b52640a4e 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -1,7 +1,12 @@ # Controller for viewing a repository's file structure -class Projects::TreeController < Projects::BaseTreeController - def show +class Projects::TreeController < Projects::ApplicationController + include ExtractsPath + before_filter :assign_ref_vars + before_filter :authorize_download_code! + before_filter :require_non_empty_project, except: [:new, :create] + + def show if tree.entries.empty? if @repository.blob_at(@commit.id, @path) redirect_to project_blob_path(@project, File.join(@ref, @path)) and return diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index e51cb30bdd..19215cfb7e 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -1,17 +1,9 @@ # Module providing methods for dealing with separating a tree-ish string and a # file path string when combined in a request parameter module ExtractsPath - extend ActiveSupport::Concern - # Raised when given an invalid file path class InvalidPathError < StandardError; end - included do - if respond_to?(:before_filter) - before_filter :assign_ref_vars - end - end - # Given a string containing both a Git tree-ish, such as a branch or tag, and # a filesystem path joined by forward slashes, attempts to separate the two. # From 59b08942aa3de480899f32f1a6f2f948329ae3fc Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 15:02:28 -0800 Subject: [PATCH 074/124] Refactor blob controllers --- .../projects/base_tree_controller.rb | 7 -- app/controllers/projects/blob_controller.rb | 102 ++++++++++++++++-- .../projects/edit_tree_controller.rb | 60 ----------- .../projects/new_tree_controller.rb | 20 ---- 4 files changed, 95 insertions(+), 94 deletions(-) delete mode 100644 app/controllers/projects/base_tree_controller.rb delete mode 100644 app/controllers/projects/edit_tree_controller.rb delete mode 100644 app/controllers/projects/new_tree_controller.rb diff --git a/app/controllers/projects/base_tree_controller.rb b/app/controllers/projects/base_tree_controller.rb deleted file mode 100644 index a7b1b7b40e..0000000000 --- a/app/controllers/projects/base_tree_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Projects::BaseTreeController < Projects::ApplicationController - include ExtractsPath - - before_filter :authorize_download_code! - before_filter :require_non_empty_project -end - diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 2412800c49..00b82ff1df 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -2,16 +2,70 @@ class Projects::BlobController < Projects::ApplicationController include ExtractsPath - # Authorize - before_filter :authorize_download_code! - before_filter :require_non_empty_project - before_filter :authorize_push_code!, only: [:destroy] + # Raised when given an invalid file path + class InvalidPathError < StandardError; end - before_filter :blob + before_filter :authorize_download_code! + before_filter :require_non_empty_project, except: [:new, :create] + before_filter :authorize_push_code!, only: [:destroy] + before_filter :assign_blob_vars + before_filter :commit, except: [:new, :create] + before_filter :blob, except: [:new, :create] + before_filter :from_merge_request, only: [:edit, :update] + before_filter :after_edit_path, only: [:edit, :update] + before_filter :require_branch_head, only: [:edit, :update] + + def new + commit unless @repository.empty? + end + + def create + file_path = File.join(@path, File.basename(params[:file_name])) + result = Files::CreateService.new(@project, current_user, params, @ref, file_path).execute + + if result[:status] == :success + flash[:notice] = "Your changes have been successfully committed" + redirect_to project_blob_path(@project, File.join(@ref, file_path)) + else + flash[:alert] = result[:message] + render :show + end + end def show end + def edit + @last_commit = Gitlab::Git::Commit.last_for_path(@repository, @ref, @path).sha + end + + def update + result = Files::UpdateService. + new(@project, current_user, params, @ref, @path).execute + + if result[:status] == :success + flash[:notice] = "Your changes have been successfully committed" + + if from_merge_request + from_merge_request.reload_code + end + + redirect_to after_edit_path + else + flash[:alert] = result[:message] + render :show + end + end + + def preview + @content = params[:content] + diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', + include_diff_info: true) + @diff_lines = Gitlab::Diff::Parser.new.parse(diffy.diff.scan(/.*\n/)) + + render layout: false + end + def destroy result = Files::DeleteService.new(@project, current_user, params, @ref, @path).execute @@ -46,10 +100,44 @@ class Projects::BlobController < Projects::ApplicationController if @blob @blob - elsif tree.entries.any? - redirect_to project_tree_path(@project, File.join(@ref, @path)) and return else + if tree = @repository.tree(@commit.id, @path) + if tree.entries.any? + redirect_to project_tree_path(@project, File.join(@ref, @path)) and return + end + end + return not_found! end end + + def commit + @commit = @repository.commit(@ref) + + return not_found! unless @commit + end + + def assign_blob_vars + @id = params[:id] + @ref, @path = extract_ref(@id) + + + rescue InvalidPathError + not_found! + end + + def after_edit_path + @after_edit_path ||= + if from_merge_request + diffs_project_merge_request_path(from_merge_request.target_project, from_merge_request) + + "#file-path-#{hexdigest(@path)}" + else + project_blob_path(@project, @id) + end + end + + def from_merge_request + # If blob edit was initiated from merge request page + @from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id]) + end end diff --git a/app/controllers/projects/edit_tree_controller.rb b/app/controllers/projects/edit_tree_controller.rb deleted file mode 100644 index 65661c8041..0000000000 --- a/app/controllers/projects/edit_tree_controller.rb +++ /dev/null @@ -1,60 +0,0 @@ -class Projects::EditTreeController < Projects::BaseTreeController - before_filter :require_branch_head - before_filter :blob - before_filter :authorize_push_code! - before_filter :from_merge_request - before_filter :after_edit_path - - def show - @last_commit = Gitlab::Git::Commit.last_for_path(@repository, @ref, @path).sha - end - - def update - result = Files::UpdateService. - new(@project, current_user, params, @ref, @path).execute - - if result[:status] == :success - flash[:notice] = "Your changes have been successfully committed" - - if from_merge_request - from_merge_request.reload_code - end - - redirect_to after_edit_path - else - flash[:alert] = result[:message] - render :show - end - end - - def preview - @content = params[:content] - - diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', - include_diff_info: true) - @diff_lines = Gitlab::Diff::Parser.new.parse(diffy.diff.scan(/.*\n/)) - - render layout: false - end - - private - - def blob - @blob ||= @repository.blob_at(@commit.id, @path) - end - - def after_edit_path - @after_edit_path ||= - if from_merge_request - diffs_project_merge_request_path(from_merge_request.target_project, from_merge_request) + - "#file-path-#{hexdigest(@path)}" - else - project_blob_path(@project, @id) - end - end - - def from_merge_request - # If blob edit was initiated from merge request page - @from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id]) - end -end diff --git a/app/controllers/projects/new_tree_controller.rb b/app/controllers/projects/new_tree_controller.rb deleted file mode 100644 index ffba706b2f..0000000000 --- a/app/controllers/projects/new_tree_controller.rb +++ /dev/null @@ -1,20 +0,0 @@ -class Projects::NewTreeController < Projects::BaseTreeController - before_filter :require_branch_head - before_filter :authorize_push_code! - - def show - end - - def update - file_path = File.join(@path, File.basename(params[:file_name])) - result = Files::CreateService.new(@project, current_user, params, @ref, file_path).execute - - if result[:status] == :success - flash[:notice] = "Your changes have been successfully committed" - redirect_to project_blob_path(@project, File.join(@ref, file_path)) - else - flash[:alert] = result[:message] - render :show - end - end -end From e07da5989f5ae14dddc130b80210342ca776e6d2 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 15:02:59 -0800 Subject: [PATCH 075/124] SEtup new routes for creating and changing repository files --- config/routes.rb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 8c3eef2326..f29b620e07 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -211,17 +211,20 @@ Gitlab::Application.routes.draw do end scope module: :projects do + # Blob routes: + get '/new/:id', to: 'blob#new', constraints: {id: /.+/}, as: 'new_blob' + post '/create/:id', to: 'blob#create', constraints: {id: /.+/}, as: 'create_blob' + get '/edit/:id', to: 'blob#edit', constraints: {id: /.+/}, as: 'edit_blob' + put '/update/:id', to: 'blob#update', constraints: {id: /.+/}, as: 'update_blob' + post '/preview/:id', to: 'blob#preview', constraints: {id: /.+/}, as: 'preview_blob' + resources :blob, only: [:show, :destroy], constraints: { id: /.+/, format: false } do get :diff, on: :member end + resources :raw, only: [:show], constraints: {id: /.+/} resources :tree, only: [:show], constraints: {id: /.+/, format: /(html|js)/ } - resources :edit_tree, only: [:show, :update], constraints: { id: /.+/ }, path: 'edit' do - # Cannot be GET to differentiate from GET paths that end in preview. - post :preview, on: :member - end resource :avatar, only: [:show, :destroy] - resources :new_tree, only: [:show, :update], constraints: {id: /.+/}, path: 'new' resources :commit, only: [:show], constraints: {id: /[[:alnum:]]{6,40}/} resources :commits, only: [:show], constraints: {id: /(?:[^.]|\.(?!atom$))+/, format: /atom/} resources :compare, only: [:index, :create] From 21297e78afd5ddfbfdf62f471acf1ab2f0c2a892 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 15:03:14 -0800 Subject: [PATCH 076/124] Refactor blob helpers --- app/helpers/blob_helper.rb | 38 ++++++++++++++++++++++++++++++++++ app/helpers/projects_helper.rb | 6 +++++- app/helpers/tree_helper.rb | 38 ---------------------------------- 3 files changed, 43 insertions(+), 39 deletions(-) diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 3a28280396..e75eebd2da 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -19,4 +19,42 @@ module BlobHelper def no_highlight_files %w(credits changelog copying copyright license authors) end + + def edit_blob_link(project, ref, path, options = {}) + blob = + begin + project.repository.blob_at(ref, path) + rescue + nil + end + + if blob && blob.text? + text = 'Edit' + after = options[:after] || '' + from_mr = options[:from_merge_request_id] + link_opts = {} + link_opts[:from_merge_request_id] = from_mr if from_mr + cls = 'btn btn-small' + if allowed_tree_edit?(project, ref) + link_to text, project_edit_blob_path(project, tree_join(ref, path), + link_opts), class: cls + else + content_tag :span, text, class: cls + ' disabled' + end + after.html_safe + else + '' + end + end + + def leave_edit_message + "Leave edit mode?\nAll unsaved changes will be lost." + end + + def editing_preview_title(filename) + if Gitlab::MarkdownHelper.previewable?(filename) + 'Preview' + else + 'Preview changes' + end + end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index de232ab4e2..9d2c99356a 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -187,7 +187,11 @@ module ProjectsHelper "Issues - " + title end elsif current_controller?(:blob) - "#{@project.path}\/#{@blob.path} at #{@ref} - " + title + if current_action?(:new) || current_action?(:create) + "New file at #{@ref}" + elsif @blob + "Edit file #{@blob.path} at #{@ref}" + end elsif current_controller?(:commits) "Commits at #{@ref} - " + title elsif current_controller?(:merge_requests) diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 1d987a6ffc..727ec3fb23 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -64,32 +64,6 @@ module TreeHelper ::Gitlab::GitAccess.can_push_to_branch?(current_user, project, ref) end - def edit_blob_link(project, ref, path, options = {}) - blob = - begin - project.repository.blob_at(ref, path) - rescue - nil - end - - if blob && blob.text? - text = 'Edit' - after = options[:after] || '' - from_mr = options[:from_merge_request_id] - link_opts = {} - link_opts[:from_merge_request_id] = from_mr if from_mr - cls = 'btn btn-small' - if allowed_tree_edit?(project, ref) - link_to text, project_edit_tree_path(project, tree_join(ref, path), - link_opts), class: cls - else - content_tag :span, text, class: cls + ' disabled' - end + after.html_safe - else - '' - end - end - def tree_breadcrumbs(tree, max_links = 2) if @path.present? part_path = "" @@ -121,16 +95,4 @@ module TreeHelper return tree.name end end - - def leave_edit_message - "Leave edit mode?\nAll unsaved changes will be lost." - end - - def editing_preview_title(filename) - if Gitlab::MarkdownHelper.previewable?(filename) - 'Preview' - else - 'Diff' - end - end end From 752cb506c033c281064abb56093c41a7dac2735a Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 15:03:30 -0800 Subject: [PATCH 077/124] Refactor blob views --- .../show.html.haml => blob/edit.html.haml} | 13 +++++++++---- .../{new_tree/show.html.haml => blob/new.html.haml} | 2 +- .../projects/{edit_tree => blob}/preview.html.haml | 0 app/views/projects/empty.html.haml | 2 +- app/views/projects/tree/_tree.html.haml | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) rename app/views/projects/{edit_tree/show.html.haml => blob/edit.html.haml} (84%) rename app/views/projects/{new_tree/show.html.haml => blob/new.html.haml} (92%) rename app/views/projects/{edit_tree => blob}/preview.html.haml (100%) diff --git a/app/views/projects/edit_tree/show.html.haml b/app/views/projects/blob/edit.html.haml similarity index 84% rename from app/views/projects/edit_tree/show.html.haml rename to app/views/projects/blob/edit.html.haml index 7e0789853a..883845c03f 100644 --- a/app/views/projects/edit_tree/show.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -1,11 +1,16 @@ .file-editor %ul.nav.nav-tabs.js-edit-mode %li.active - = link_to 'Edit', '#editor' - %li - = link_to editing_preview_title(@blob.name), '#preview', 'data-preview-url' => preview_project_edit_tree_path(@project, @id) + = link_to '#editor' do + %i.fa.fa-edit + Edit file - = form_tag(project_edit_tree_path(@project, @id), method: :put, class: "form-horizontal") do + %li + = link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id) do + %i.fa.fa-eye + = editing_preview_title(@blob.name) + + = form_tag(project_update_blob_path(@project, @id), method: :put, class: "form-horizontal") do = render 'projects/blob_editor', ref: @ref, path: @path, blob_data: @blob.data = render 'shared/commit_message_container', params: params, placeholder: "Update #{@blob.name}" diff --git a/app/views/projects/new_tree/show.html.haml b/app/views/projects/blob/new.html.haml similarity index 92% rename from app/views/projects/new_tree/show.html.haml rename to app/views/projects/blob/new.html.haml index cf7b768694..57e830d5c5 100644 --- a/app/views/projects/new_tree/show.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,7 +1,7 @@ %h3.page-title New file %hr .file-editor - = form_tag(project_new_tree_path(@project, @id), method: :put, class: 'form-horizontal form-new-file') do + = form_tag(project_create_blob_path(@project, @id), method: :post, class: 'form-horizontal form-new-file') do .form-group.commit_message-group = label_tag 'file_name', class: 'control-label' do File name diff --git a/app/views/projects/edit_tree/preview.html.haml b/app/views/projects/blob/preview.html.haml similarity index 100% rename from app/views/projects/edit_tree/preview.html.haml rename to app/views/projects/blob/preview.html.haml diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 3a42fce43e..776a7327bc 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -8,7 +8,7 @@ The repository for this project is empty %p.lead You can - = link_to '#', class: 'btn btn-new btn-lg' do + = link_to project_new_blob_path(@project, 'master'), class: 'btn btn-new btn-lg' do add a file  or push it via command line. diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree.html.haml index 68ccd4d61b..f902440b3f 100644 --- a/app/views/projects/tree/_tree.html.haml +++ b/app/views/projects/tree/_tree.html.haml @@ -10,7 +10,7 @@ = link_to title, '#' - if current_user && can_push_branch?(@project, @ref) %li - = link_to project_new_tree_path(@project, @id), title: 'New file', id: 'new-file-link' do + = link_to project_new_blob_path(@project, @id), title: 'New file', id: 'new-file-link' do %small %i.fa.fa-plus From ed9137862773c8cd242e16a7945cf18a0b2e1ff9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 15:12:13 -0800 Subject: [PATCH 078/124] Fix blob controller rendering in case of errors --- app/controllers/projects/blob_controller.rb | 4 ++-- features/steps/shared/paths.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 00b82ff1df..b471d57f69 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -28,7 +28,7 @@ class Projects::BlobController < Projects::ApplicationController redirect_to project_blob_path(@project, File.join(@ref, file_path)) else flash[:alert] = result[:message] - render :show + render :new end end @@ -53,7 +53,7 @@ class Projects::BlobController < Projects::ApplicationController redirect_to after_edit_path else flash[:alert] = result[:message] - render :show + render :edit end end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 33ef6ccacf..cef48c179b 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -284,11 +284,11 @@ module SharedPaths end step 'I am on the new file page' do - current_path.should eq(project_new_tree_path(@project, root_ref)) + current_path.should eq(project_create_blob_path(@project, root_ref)) end step 'I am on the ".gitignore" edit file page' do - current_path.should eq(project_edit_tree_path( + current_path.should eq(project_edit_blob_path( @project, File.join(root_ref, '.gitignore'))) end From 2b8b060236535d3abe27d705be97523b0816da67 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 15:15:51 -0800 Subject: [PATCH 079/124] Make code font size a bit bigger so its easier to read it --- app/assets/stylesheets/generic/highlight.scss | 8 ++++---- app/assets/stylesheets/main/variables.scss | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/generic/highlight.scss b/app/assets/stylesheets/generic/highlight.scss index 83dc7ab491..839551ca8d 100644 --- a/app/assets/stylesheets/generic/highlight.scss +++ b/app/assets/stylesheets/generic/highlight.scss @@ -10,8 +10,8 @@ border: none; border-radius: 0; font-family: $monospace_font; - font-size: 12px !important; - line-height: 16px !important; + font-size: $code_font_size !important; + line-height: 1.4 !important; margin: 0; overflow: auto; overflow-y: hidden; @@ -38,8 +38,8 @@ a { font-family: $monospace_font; display: block; - font-size: 12px !important; - line-height: 16px !important; + font-size: $code_font_size !important; + line-height: 1.4 !important; white-space: nowrap; i { diff --git a/app/assets/stylesheets/main/variables.scss b/app/assets/stylesheets/main/variables.scss index 6bbce70a78..f2402a4fc3 100644 --- a/app/assets/stylesheets/main/variables.scss +++ b/app/assets/stylesheets/main/variables.scss @@ -59,3 +59,4 @@ $list-font-size: 15px; $sidebar_width: 230px; $avatar_radius: 50%; +$code_font_size: 13px; From 4641514cbfdcc56a0cbc5ad3444a92284df9a665 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 26 Jan 2015 15:59:40 -0800 Subject: [PATCH 080/124] Update rspec tests to the new external issue logic. --- app/assets/javascripts/dispatcher.js.coffee | 1 - app/assets/javascripts/project_new.js.coffee | 14 ------------- spec/factories/projects.rb | 16 +++++++++++++-- spec/helpers/gitlab_markdown_helper_spec.rb | 11 ++++++---- spec/helpers/issues_helper_spec.rb | 21 ++++++++++---------- spec/helpers/projects_helper_spec.rb | 21 -------------------- 6 files changed, 31 insertions(+), 53 deletions(-) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index ef86c2781c..9457f88817 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -99,7 +99,6 @@ class Dispatcher switch path[1] when 'edit' shortcut_handler = new ShortcutsNavigation() - new ProjectNew() when 'new' new ProjectNew() when 'show' diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee index f4a2ca813d..836269c44f 100644 --- a/app/assets/javascripts/project_new.js.coffee +++ b/app/assets/javascripts/project_new.js.coffee @@ -9,17 +9,3 @@ class @ProjectNew initEvents: -> disableButtonIfEmptyField '#project_name', '.project-submit' - - $('#project_issues_enabled').change -> - if ($(this).is(':checked') == true) - $('#project_issues_tracker').removeAttr('disabled') - else - $('#project_issues_tracker').attr('disabled', 'disabled') - - $('#project_issues_tracker').change() - - $('#project_issues_tracker').change -> - if ($(this).val() == gon.default_issues_tracker || $(this).is(':disabled')) - $('#project_issues_tracker_id').attr('disabled', 'disabled') - else - $('#project_issues_tracker_id').removeAttr('disabled') diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 1738b20fab..499139089d 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -76,7 +76,19 @@ FactoryGirl.define do end factory :redmine_project, parent: :project do - issues_tracker { "redmine" } - issues_tracker_id { "project_name_in_redmine" } + after :create do |project| + project.create_redmine_service( + active: true, + properties: { + project_url: 'http://redmine/projects/project_name_in_redmine', + issues_url: "http://redmine/#{project.id}/project_name_in_redmine/:id", + new_issue_url: 'http://redmine/projects/project_name_in_redmine/issues/new' + } + ) + end + after :create do |project| + project.issues_tracker = 'redmine' + project.issues_tracker_id = 'project_name_in_redmine' + end end end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 86ba801ce0..5c9eea956f 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -296,10 +296,13 @@ describe GitlabMarkdownHelper do let(:reference) { "JIRA-#{issue.iid}" } before do - issue_tracker_config = { "jira" => { "title" => "JIRA tracker", "issues_url" => "http://jira.example/browse/:id" } } - Gitlab.config.stub(:issues_tracker).and_return(issue_tracker_config) - @project.stub(:issues_tracker).and_return("jira") - @project.stub(:issues_tracker_id).and_return("JIRA") + jira = @project.create_jira_service if @project.jira_service.nil? + properties = {"title"=>"JIRA tracker", "project_url"=>"http://jira.example/issues/?jql=project=A", "issues_url"=>"http://jira.example/browse/:id", "new_issue_url"=>"http://jira.example/secure/CreateIssue.jspa"} + jira.update_attributes(properties: properties, active: true) + end + + after do + @project.jira_service.destroy! unless @project.jira_service.nil? end it "should link using a valid id" do diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index 9c95bc044f..c82729a52e 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -24,7 +24,7 @@ describe IssuesHelper do end describe :url_for_project_issues do - let(:project_url) { Gitlab.config.issues_tracker.redmine.project_url} + let(:project_url) { ext_project.external_issue_tracker.project_url } let(:ext_expected) do project_url.gsub(':project_id', ext_project.id.to_s) .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s) @@ -54,17 +54,16 @@ describe IssuesHelper do Gitlab.config.stub(:issues_tracker).and_return(nil) end - it "should return path to internal tracker" do - url_for_project_issues.should match(polymorphic_path([@project])) + it "should return path to external tracker" do + url_for_project_issues.should match(ext_expected) end end end describe :url_for_issue do - let(:issue_id) { 3 } - let(:issues_url) { Gitlab.config.issues_tracker.redmine.issues_url} + let(:issues_url) { ext_project.external_issue_tracker.issues_url} let(:ext_expected) do - issues_url.gsub(':id', issue_id.to_s) + issues_url.gsub(':id', issue.iid.to_s) .gsub(':project_id', ext_project.id.to_s) .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s) end @@ -78,7 +77,7 @@ describe IssuesHelper do it "should return path to external tracker" do @project = ext_project - url_for_issue(issue_id).should match(ext_expected) + url_for_issue(issue.iid).should match(ext_expected) end it "should return empty string if project nil" do @@ -93,14 +92,14 @@ describe IssuesHelper do Gitlab.config.stub(:issues_tracker).and_return(nil) end - it "should return internal path" do - url_for_issue(issue.iid).should match(polymorphic_path([@project, issue])) + it "should return external path" do + url_for_issue(issue.iid).should match(ext_expected) end end end describe :url_for_new_issue do - let(:issues_url) { Gitlab.config.issues_tracker.redmine.new_issue_url} + let(:issues_url) { ext_project.external_issue_tracker.new_issue_url } let(:ext_expected) do issues_url.gsub(':project_id', ext_project.id.to_s) .gsub(':issues_tracker_id', ext_project.issues_tracker_id.to_s) @@ -131,7 +130,7 @@ describe IssuesHelper do end it "should return internal path" do - url_for_new_issue.should match(new_project_issue_path(@project)) + url_for_new_issue.should match(ext_expected) end end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 2146b0b138..281d486219 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -1,32 +1,11 @@ require 'spec_helper' describe ProjectsHelper do - describe '#project_issues_trackers' do - it "returns the correct issues trackers available" do - project_issues_trackers.should == - "\n" \ - "" - end - - it "returns the correct issues trackers available with current tracker 'gitlab' selected" do - project_issues_trackers('gitlab').should == - "\n" \ - "" - end - - it "returns the correct issues trackers available with current tracker 'redmine' selected" do - project_issues_trackers('redmine').should == - "\n" \ - "" - end - end - describe "#project_status_css_class" do it "returns appropriate class" do project_status_css_class("started").should == "active" project_status_css_class("failed").should == "danger" project_status_css_class("finished").should == "success" end - end end From 9b5b334a798233b200318ecfbff55f0284f874da Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 26 Jan 2015 16:04:08 -0800 Subject: [PATCH 081/124] Remove unused feature steps. --- features/project/edit_issuetracker.feature | 18 ------------ features/steps/project/issue_tracker.rb | 31 --------------------- spec/lib/gitlab/reference_extractor_spec.rb | 1 - 3 files changed, 50 deletions(-) delete mode 100644 features/project/edit_issuetracker.feature delete mode 100644 features/steps/project/issue_tracker.rb diff --git a/features/project/edit_issuetracker.feature b/features/project/edit_issuetracker.feature deleted file mode 100644 index cc0de07ca6..0000000000 --- a/features/project/edit_issuetracker.feature +++ /dev/null @@ -1,18 +0,0 @@ -Feature: Project Issue Tracker - Background: - Given I sign in as a user - And I own project "Shop" - And project "Shop" has issues enabled - And I visit project "Shop" page - - Scenario: I set the issue tracker to "GitLab" - When I visit edit project "Shop" page - And change the issue tracker to "GitLab" - And I save project - Then I the project should have "GitLab" as issue tracker - - Scenario: I set the issue tracker to "Redmine" - When I visit edit project "Shop" page - And change the issue tracker to "Redmine" - And I save project - Then I the project should have "Redmine" as issue tracker diff --git a/features/steps/project/issue_tracker.rb b/features/steps/project/issue_tracker.rb deleted file mode 100644 index e170029270..0000000000 --- a/features/steps/project/issue_tracker.rb +++ /dev/null @@ -1,31 +0,0 @@ -class Spinach::Features::ProjectIssueTracker < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - - step 'project "Shop" has issues enabled' do - @project = Project.find_by(name: "Shop") - @project ||= create(:project, name: "Shop", namespace: @user.namespace) - @project.issues_enabled = true - end - - step 'change the issue tracker to "GitLab"' do - select 'GitLab', from: 'project_issues_tracker' - end - - step 'I the project should have "GitLab" as issue tracker' do - find_field('project_issues_tracker').value.should == 'gitlab' - end - - step 'change the issue tracker to "Redmine"' do - select 'Redmine', from: 'project_issues_tracker' - end - - step 'I the project should have "Redmine" as issue tracker' do - find_field('project_issues_tracker').value.should == 'redmine' - end - - step 'I save project' do - click_button 'Save changes' - end -end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 23867df39d..5f45df4e8c 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -12,7 +12,6 @@ describe Gitlab::ReferenceExtractor do end it 'extracts JIRA issue references' do - Gitlab.config.gitlab.stub(:issues_tracker).and_return('jira') subject.analyze('this one talks about issue JIRA-1234', nil) subject.issues.should == [{ project: nil, id: 'JIRA-1234' }] end From 90ba3a385cda545a58b3bb7893898bc16e982a73 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 16:08:59 -0800 Subject: [PATCH 082/124] Fix tests for blobs refactoring --- features/steps/project/source/browse_files.rb | 2 +- spec/routing/project_routing_spec.rb | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 805e6ff0ea..1caad73654 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -78,7 +78,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I click link "Diff"' do - click_link 'Diff' + click_link 'Preview changes' end step 'I click on "Commit Changes"' do diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 8191d1fb9c..e36b266a1f 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -430,21 +430,17 @@ describe Projects::TreeController, 'routing' do end end -describe Projects::EditTreeController, 'routing' do - it 'to #show' do +describe Projects::BlobController, 'routing' do + it 'to #edit' do get('/gitlab/gitlabhq/edit/master/app/models/project.rb').should( - route_to('projects/edit_tree#show', + route_to('projects/blob#edit', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb')) - get('/gitlab/gitlabhq/edit/master/app/models/project.rb/preview').should( - route_to('projects/edit_tree#show', - project_id: 'gitlab/gitlabhq', - id: 'master/app/models/project.rb/preview')) end it 'to #preview' do - post('/gitlab/gitlabhq/edit/master/app/models/project.rb/preview').should( - route_to('projects/edit_tree#preview', + post('/gitlab/gitlabhq/preview/master/app/models/project.rb').should( + route_to('projects/blob#preview', project_id: 'gitlab/gitlabhq', id: 'master/app/models/project.rb')) end From ed8c3e2d738312c09e336fb8d549eea7d9cc71b9 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 16:10:34 -0800 Subject: [PATCH 083/124] Remove unnecessary reload in test --- spec/services/merge_requests/update_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 0cd822bcda..0e60baae2c 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -48,7 +48,7 @@ describe MergeRequests::UpdateService do end it 'should create system note about merge_request reassign' do - note = @merge_request.notes.reload.last + note = @merge_request.notes.last note.note.should include "Reassigned to \@#{user2.username}" end end From 00a0d5aeeaf19ea4d72fd1890afac099026f1706 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 26 Jan 2015 16:24:11 -0800 Subject: [PATCH 084/124] Move repetition to the parent. --- .../project_services/issue_tracker_service.rb | 44 +++++++++++++++ app/models/project_services/jira_service.rb | 45 ---------------- .../project_services/redmine_service.rb | 54 ++++--------------- lib/gitlab/markdown.rb | 9 ++-- 4 files changed, 58 insertions(+), 94 deletions(-) diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 664b55a595..7ff6e0f284 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -15,4 +15,48 @@ class IssueTrackerService < Service def new_issue_url # implement inside child end + + def fields + [ + { type: 'text', name: 'description', placeholder: description }, + { type: 'text', name: 'project_url', placeholder: 'Project url' }, + { type: 'text', name: 'issues_url', placeholder: 'Issue url'}, + { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url'} + ] + end + + def initialize_properties + if properties.nil? + if enabled_in_gitlab_config + self.properties = { + title: issues_tracker['title'], + project_url: set_project_url, + issues_url: issues_tracker['issues_url'], + new_issue_url: issues_tracker['new_issue_url'] + } + end + end + end + + private + + def enabled_in_gitlab_config + Gitlab.config.issues_tracker && + Gitlab.config.issues_tracker.values.any? && + issues_tracker + end + + def issues_tracker + Gitlab.config.issues_tracker[to_param] + end + + def set_project_url + id = self.project.issues_tracker_id + + if id + issues_tracker['project_url'].gsub(":issues_tracker_id", id) + else + issues_tracker['project_url'] + end + end end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index f8b04ddeea..b0d668948d 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -21,49 +21,4 @@ class JiraService < IssueTrackerService def to_param 'jira' end - - def fields - [ - { type: 'text', name: 'title', placeholder: title }, - { type: 'text', name: 'description', placeholder: description }, - { type: 'text', name: 'project_url', placeholder: 'Project url' }, - { type: 'text', name: 'issues_url', placeholder: 'Issue url'}, - { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url'} - ] - end - - def initialize_properties - if properties.nil? - if enabled_in_gitlab_config - self.properties = { - title: issues_tracker['title'], - project_url: set_project_url, - issues_url: issues_tracker['issues_url'], - new_issue_url: issues_tracker['new_issue_url'] - } - end - end - end - - private - - def enabled_in_gitlab_config - Gitlab.config.issues_tracker && - Gitlab.config.issues_tracker.values.any? && - issues_tracker - end - - def issues_tracker - Gitlab.config.issues_tracker['jira'] - end - - def set_project_url - id = self.project.issues_tracker_id - - if id - issues_tracker['project_url'].gsub(":issues_tracker_id", id) - else - issues_tracker['project_url'] - end - end end diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index 71286d74b5..11cce3e056 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -3,56 +3,22 @@ class RedmineService < IssueTrackerService prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url def title - 'Redmine' + if self.properties && self.properties['title'].present? + self.properties['title'] + else + 'Redmine' + end end def description - 'Redmine issue tracker' + if self.properties && self.properties['description'].present? + self.properties['description'] + else + 'Redmine issue tracker' + end end def to_param 'redmine' end - - def fields - [ - { type: 'text', name: 'project_url', placeholder: 'Project url' }, - { type: 'text', name: 'issues_url', placeholder: 'Issue url'}, - { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url'} - ] - end - - def initialize_properties - if properties.nil? - if enabled_in_gitlab_config - self.properties = { - title: issues_tracker['title'], - project_url: set_project_url, - issues_url: issues_tracker['issues_url'], - new_issue_url: issues_tracker['new_issue_url'] - } - end - end - end - - private - - def enabled_in_gitlab_config - Gitlab.config.issues_tracker && - Gitlab.config.issues_tracker.values.any? && - issues_tracker - end - - def issues_tracker - Gitlab.config.issues_tracker['redmine'] - end - - def set_project_url - id = self.project.issues_tracker_id - if id - issues_tracker['project_url'].gsub(":issues_tracker_id", id) - else - issues_tracker['project_url'] - end - end end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 2f04133647..c0e83fb307 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -220,9 +220,8 @@ module Gitlab link_to("#{prefix_text}##{identifier}", url, options) end else - external_issue_tracker = project.external_issue_tracker - if external_issue_tracker.present? - reference_external_issue(identifier, external_issue_tracker, project, + if project.external_issue_tracker.present? + reference_external_issue(identifier, project, prefix_text) end end @@ -266,10 +265,10 @@ module Gitlab end end - def reference_external_issue(identifier, issue_tracker, project = @project, + def reference_external_issue(identifier, project = @project, prefix_text = nil) url = url_for_issue(identifier, project) - title = issue_tracker.title + title = project.external_issue_tracker.title options = html_options.merge( title: "Issue in #{title}", From e6b97d09470b01b5b65e87dab339c500f1bac45f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 18:56:56 -0800 Subject: [PATCH 085/124] Improve font sizes for code and diff --- CHANGELOG | 4 ++-- app/assets/stylesheets/generic/highlight.scss | 4 ++-- app/assets/stylesheets/main/variables.scss | 1 + app/assets/stylesheets/sections/diff.scss | 6 +++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 72ca2b529d..91409707c8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -17,8 +17,8 @@ v 7.8.0 - Show tags in commit view (Hannes Rosenögger) - Only count a user's vote once on a merge request or issue (Michael Clarke) - - - - - + - Increate font size when browse source files and diffs + - Create new file in empty repository using GitLab UI - - Upgrade Sidekiq gem to version 3.3.0 - Stop git zombie creation during force push check diff --git a/app/assets/stylesheets/generic/highlight.scss b/app/assets/stylesheets/generic/highlight.scss index 839551ca8d..e1ca86af81 100644 --- a/app/assets/stylesheets/generic/highlight.scss +++ b/app/assets/stylesheets/generic/highlight.scss @@ -11,7 +11,7 @@ border-radius: 0; font-family: $monospace_font; font-size: $code_font_size !important; - line-height: 1.4 !important; + line-height: $code_line_height !important; margin: 0; overflow: auto; overflow-y: hidden; @@ -39,7 +39,7 @@ font-family: $monospace_font; display: block; font-size: $code_font_size !important; - line-height: 1.4 !important; + line-height: $code_line_height !important; white-space: nowrap; i { diff --git a/app/assets/stylesheets/main/variables.scss b/app/assets/stylesheets/main/variables.scss index f2402a4fc3..acbf5be94a 100644 --- a/app/assets/stylesheets/main/variables.scss +++ b/app/assets/stylesheets/main/variables.scss @@ -60,3 +60,4 @@ $sidebar_width: 230px; $avatar_radius: 50%; $code_font_size: 13px; +$code_line_height: 1.5; diff --git a/app/assets/stylesheets/sections/diff.scss b/app/assets/stylesheets/sections/diff.scss index 758f15c801..da50dbe471 100644 --- a/app/assets/stylesheets/sections/diff.scss +++ b/app/assets/stylesheets/sections/diff.scss @@ -37,7 +37,7 @@ overflow-y: hidden; background: #FFF; color: #333; - font-size: 12px; + font-size: $code_font_size; .old { span.idiff { background-color: #F99; @@ -64,8 +64,8 @@ margin: 0px; padding: 0px; td { - line-height: 18px; - font-size: 12px; + line-height: $code_line_height; + font-size: $code_font_size; } } From aac36b120ef86469feb05ae5db39205493f851ed Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 19:28:11 -0800 Subject: [PATCH 086/124] Fix app title when browse blob --- app/helpers/projects_helper.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 9d2c99356a..db4bb303d0 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -189,6 +189,8 @@ module ProjectsHelper elsif current_controller?(:blob) if current_action?(:new) || current_action?(:create) "New file at #{@ref}" + elsif current_action?(:show) + "#{@blob.path} at #{@ref}" elsif @blob "Edit file #{@blob.path} at #{@ref}" end From a3d879d427c1236d26832dcd0312b3e0d6158bbe Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 20:57:42 -0800 Subject: [PATCH 087/124] Refactor web editor --- .../javascripts/{ => blob}/blob.js.coffee | 0 .../javascripts/blob/edit_blob.js.coffee | 40 ++++++++++++++++++ .../javascripts/blob/new_blob.js.coffee | 17 ++++++++ app/assets/stylesheets/sections/editor.scss | 22 ++++++++++ app/views/projects/_blob_editor.html.haml | 15 ------- app/views/projects/blob/_editor.html.haml | 25 +++++++++++ app/views/projects/blob/edit.html.haml | 42 +------------------ app/views/projects/blob/new.html.haml | 30 +------------ lib/gitlab/satellite/files/new_file_action.rb | 8 ++-- 9 files changed, 112 insertions(+), 87 deletions(-) rename app/assets/javascripts/{ => blob}/blob.js.coffee (100%) create mode 100644 app/assets/javascripts/blob/edit_blob.js.coffee create mode 100644 app/assets/javascripts/blob/new_blob.js.coffee delete mode 100644 app/views/projects/_blob_editor.html.haml create mode 100644 app/views/projects/blob/_editor.html.haml diff --git a/app/assets/javascripts/blob.js.coffee b/app/assets/javascripts/blob/blob.js.coffee similarity index 100% rename from app/assets/javascripts/blob.js.coffee rename to app/assets/javascripts/blob/blob.js.coffee diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee new file mode 100644 index 0000000000..79433dab50 --- /dev/null +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -0,0 +1,40 @@ +class @EditBlob + constructor: (assets_path, mode)-> + ace.config.set "modePath", assets_path + '/ace' + ace.config.loadModule "ace/ext/searchbox" + if mode + ace_mode = mode + editor = ace.edit("editor") + editor.focus() + + if ace_mode + editor.getSession().setMode "ace/mode/" + ace_mode + + disableButtonIfEmptyField "#commit_message", ".js-commit-button" + $(".js-commit-button").click -> + $("#file-content").val editor.getValue() + $(".file-editor form").submit() + return + + editModePanes = $(".js-edit-mode-pane") + editModeLinks = $(".js-edit-mode a") + editModeLinks.click (event) -> + event.preventDefault() + currentLink = $(this) + paneId = currentLink.attr("href") + currentPane = editModePanes.filter(paneId) + editModeLinks.parent().removeClass "active hover" + currentLink.parent().addClass "active hover" + editModePanes.hide() + if paneId is "#preview" + currentPane.fadeIn 200 + $.post currentLink.data("preview-url"), + content: editor.getValue() + , (response) -> + currentPane.empty().append response + return + + else + currentPane.fadeIn 200 + editor.focus() + return diff --git a/app/assets/javascripts/blob/new_blob.js.coffee b/app/assets/javascripts/blob/new_blob.js.coffee new file mode 100644 index 0000000000..ed4b7c4793 --- /dev/null +++ b/app/assets/javascripts/blob/new_blob.js.coffee @@ -0,0 +1,17 @@ +class @NewBlob + constructor: (assets_path, mode)-> + ace.config.set "modePath", assets_path + '/ace' + ace.config.loadModule "ace/ext/searchbox" + if mode + ace_mode = mode + editor = ace.edit("editor") + editor.focus() + + if ace_mode + editor.getSession().setMode "ace/mode/" + ace_mode + + disableButtonIfEmptyField "#commit_message", ".js-commit-button" + $(".js-commit-button").click -> + $("#file-content").val editor.getValue() + $(".file-editor form").submit() + return diff --git a/app/assets/stylesheets/sections/editor.scss b/app/assets/stylesheets/sections/editor.scss index f62f46ee16..becd593331 100644 --- a/app/assets/stylesheets/sections/editor.scss +++ b/app/assets/stylesheets/sections/editor.scss @@ -31,4 +31,26 @@ margin: 5px 8px 0 8px; } } + + .file-title { + @extend .monospace; + font-size: 14px; + } + + .editor-ref { + background: #fafafa; + padding: 18px 15px; + padding-left: 25px; + border-right: 1px solid #CCC; + display: inline-block; + margin: -10px -15px; + margin-right: 10px; + } + + .editor-file-name { + .new-file-name { + display: inline-block; + width: 200px; + } + } } diff --git a/app/views/projects/_blob_editor.html.haml b/app/views/projects/_blob_editor.html.haml deleted file mode 100644 index 1fb74b55c4..0000000000 --- a/app/views/projects/_blob_editor.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -.file-holder.file - .file-title - %i.icon-file - %span.file_name - %span.monospace.light #{ref} - - if local_assigns[:path] - = ': ' + local_assigns[:path] - .file-content.code - %pre.js-edit-mode-pane#editor - = params[:content] || local_assigns[:blob_data] - - if local_assigns[:path] - .js-edit-mode-pane#preview.hide - .center - %h2 - %i.icon-spinner.icon-spin diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml new file mode 100644 index 0000000000..a0d9ea57b1 --- /dev/null +++ b/app/views/projects/blob/_editor.html.haml @@ -0,0 +1,25 @@ +.file-holder.file + .file-title + .editor-ref + %i.fa.fa-code-fork + = ref + %span.editor-file-name + - if @path + %span.monospace + = @path + + - if current_action?(:new) || current_action?(:create) + \/ + = text_field_tag 'file_name', params[:file_name], placeholder: "sample.rb", + required: true, class: 'form-control new-file-name' + .pull-right + = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'form-control' + + .file-content.code + %pre.js-edit-mode-pane#editor + = params[:content] || local_assigns[:blob_data] + - if local_assigns[:path] + .js-edit-mode-pane#preview.hide + .center + %h2 + %i.icon-spinner.icon-spin diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 883845c03f..c0734d9f47 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -11,7 +11,7 @@ = editing_preview_title(@blob.name) = form_tag(project_update_blob_path(@project, @id), method: :put, class: "form-horizontal") do - = render 'projects/blob_editor', ref: @ref, path: @path, blob_data: @blob.data + = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data = render 'shared/commit_message_container', params: params, placeholder: "Update #{@blob.name}" = hidden_field_tag 'last_commit', @last_commit @@ -21,42 +21,4 @@ cancel_path: @after_edit_path :javascript - ace.config.set("modePath", gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}/ace") - ace.config.loadModule("ace/ext/searchbox"); - var ace_mode = "#{@blob.language.try(:ace_mode)}"; - var editor = ace.edit("editor"); - if (ace_mode) { - editor.getSession().setMode('ace/mode/' + ace_mode); - } - - disableButtonIfEmptyField("#commit_message", ".js-commit-button"); - - $(".js-commit-button").click(function(){ - $("#file-content").val(editor.getValue()); - $(".file-editor form").submit(); - }); - - var editModePanes = $('.js-edit-mode-pane'), - editModeLinks = $('.js-edit-mode a'); - - editModeLinks.click(function(event) { - event.preventDefault(); - - var currentLink = $(this), - paneId = currentLink.attr('href'), - currentPane = editModePanes.filter(paneId); - - editModeLinks.parent().removeClass('active hover'); - currentLink.parent().addClass('active hover'); - editModePanes.hide(); - - if (paneId == '#preview') { - currentPane.fadeIn(200); - $.post(currentLink.data('preview-url'), { content: editor.getValue() }, function(response) { - currentPane.empty().append(response); - }) - } else { - currentPane.fadeIn(200); - editor.focus() - } - }) + new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}") diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 57e830d5c5..70f52332cc 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,25 +1,7 @@ %h3.page-title New file -%hr .file-editor = form_tag(project_create_blob_path(@project, @id), method: :post, class: 'form-horizontal form-new-file') do - .form-group.commit_message-group - = label_tag 'file_name', class: 'control-label' do - File name - .col-sm-10 - .input-group - %span.input-group-addon - = @path[-1] == "/" ? @path : @path + "/" - = text_field_tag 'file_name', params[:file_name], placeholder: "sample.rb", required: true, class: 'form-control' - %span.input-group-addon - on - %span= @ref - - .form-group.commit_message-group - = label_tag :encoding, class: "control-label" do - Encoding - .col-sm-10 - = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'form-control' - = render 'projects/blob_editor', ref: @ref + = render 'projects/blob/editor', ref: @ref = render 'shared/commit_message_container', params: params, placeholder: 'Add new file' = hidden_field_tag 'content', '', id: 'file-content' @@ -27,12 +9,4 @@ cancel_path: project_tree_path(@project, @id) :javascript - ace.config.set("modePath", gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}/ace-src-noconflict") - var editor = ace.edit("editor"); - - disableButtonIfAnyEmptyField($('.form-new-file'), '.form-control', '.btn-create') - - $(".js-commit-button").click(function(){ - $("#file-content").val(editor.getValue()); - $(".file-editor form").submit(); - }); + new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null) diff --git a/lib/gitlab/satellite/files/new_file_action.rb b/lib/gitlab/satellite/files/new_file_action.rb index c230239d39..5b657c7aba 100644 --- a/lib/gitlab/satellite/files/new_file_action.rb +++ b/lib/gitlab/satellite/files/new_file_action.rb @@ -15,12 +15,12 @@ module Gitlab # create target branch in satellite at the corresponding commit from bare repo current_ref = - if repo.commits.any? - repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}") - ref - else + if @project.empty_repo? # skip this step if we want to add first file to empty repo Satellite::PARKING_BRANCH + else + repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}") + ref end file_path_in_satellite = File.join(repo.working_dir, file_path) From 8ecf7d3207ca52d694f6d8ebd9bed96041c49a8c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 20:58:38 -0800 Subject: [PATCH 088/124] Improve web editor filename placeholder --- app/views/projects/blob/_editor.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index a0d9ea57b1..96f188e4aa 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -10,7 +10,7 @@ - if current_action?(:new) || current_action?(:create) \/ - = text_field_tag 'file_name', params[:file_name], placeholder: "sample.rb", + = text_field_tag 'file_name', params[:file_name], placeholder: "File name", required: true, class: 'form-control new-file-name' .pull-right = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'form-control' From 33913f9b8fef1f8df45dc26239faf8fa4cffc982 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 26 Jan 2015 22:08:27 -0800 Subject: [PATCH 089/124] Make issue tracker service fields required. --- app/models/project.rb | 2 +- .../project_services/issue_tracker_service.rb | 4 + spec/factories/projects.rb | 6 +- spec/models/jira_service_spec.rb | 83 +++++++++++++++++++ 4 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 spec/models/jira_service_spec.rb diff --git a/app/models/project.rb b/app/models/project.rb index de31f14b98..43b61897a3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -319,7 +319,7 @@ class Project < ActiveRecord::Base end def external_issues_trackers - services.select { |service| service.category == :issue_tracker } + services.select { |service| service.issue_tracker? } end def external_issue_tracker diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 7ff6e0f284..fc7b2fe5ac 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -1,5 +1,7 @@ class IssueTrackerService < Service + validates :project_url, :issues_url, :new_issue_url, presence: true, if: :activated? + def category :issue_tracker end @@ -34,6 +36,8 @@ class IssueTrackerService < Service issues_url: issues_tracker['issues_url'], new_issue_url: issues_tracker['new_issue_url'] } + else + self.properties = {} end end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 499139089d..5ae57718c1 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -80,9 +80,9 @@ FactoryGirl.define do project.create_redmine_service( active: true, properties: { - project_url: 'http://redmine/projects/project_name_in_redmine', - issues_url: "http://redmine/#{project.id}/project_name_in_redmine/:id", - new_issue_url: 'http://redmine/projects/project_name_in_redmine/issues/new' + 'project_url' => 'http://redmine/projects/project_name_in_redmine', + 'issues_url' => "http://redmine/#{project.id}/project_name_in_redmine/:id", + 'new_issue_url' => 'http://redmine/projects/project_name_in_redmine/issues/new' } ) end diff --git a/spec/models/jira_service_spec.rb b/spec/models/jira_service_spec.rb new file mode 100644 index 0000000000..0c73a68c92 --- /dev/null +++ b/spec/models/jira_service_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe JiraService do + describe "Associations" do + it { should belong_to :project } + it { should have_one :service_hook } + end + + describe "Validations" do + context "active" do + before do + subject.active = true + end + + it { should validate_presence_of :project_url } + it { should validate_presence_of :issues_url } + it { should validate_presence_of :new_issue_url } + end + end + + describe 'description and title' do + let(:project) { create(:project) } + + context 'when it is not set' do + before do + @service = project.create_jira_service(active: true) + end + + after do + @service.destroy! + end + + it 'should be initialized' do + expect(@service.title).to eq('JIRA') + expect(@service.description).to eq("Jira issue tracker") + end + end + + context 'when it is set' do + before do + properties = { 'title' => 'Jira One', 'description' => 'Jira One issue tracker' } + @service = project.create_jira_service(active: true, properties: properties) + end + + after do + @service.destroy! + end + + it "should be correct" do + expect(@service.title).to eq('Jira One') + expect(@service.description).to eq('Jira One issue tracker') + end + end + end + + describe 'project and issue urls' do + let(:project) { create(:project) } + + context 'when gitlab.yml was initialized' do + before do + settings = { "jira" => { + "title" => "Jira", + "project_url" => "http://jira.sample/projects/project_a", + "issues_url" => "http://jira.sample/issues/:id", + "new_issue_url" => "http://jira.sample/projects/project_a/issues/new" + } + } + Gitlab.config.stub(:issues_tracker).and_return(settings) + @service = project.create_jira_service(active: true) + end + + after do + @service.destroy! + end + + it 'should be prepopulated with the settings' do + expect(@service.properties[:project_url]).to eq('http://jira.sample/projects/project_a') + expect(@service.properties[:issues_url]).to eq("http://jira.sample/issues/:id") + expect(@service.properties[:new_issue_url]).to eq("http://jira.sample/projects/project_a/issues/new") + end + end + end +end From 65e88f1e1aa247c9a89af82717aae791786ad276 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Mon, 26 Jan 2015 22:39:48 -0800 Subject: [PATCH 090/124] Fixed few tests and improved css style --- app/assets/javascripts/blob/edit_blob.js.coffee | 4 ++++ app/assets/javascripts/blob/new_blob.js.coffee | 4 ++++ app/assets/stylesheets/sections/editor.scss | 8 ++++---- app/views/projects/blob/edit.html.haml | 2 +- app/views/projects/blob/new.html.haml | 2 +- features/steps/project/source/browse_files.rb | 5 ++--- 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee index 79433dab50..6914ca759f 100644 --- a/app/assets/javascripts/blob/edit_blob.js.coffee +++ b/app/assets/javascripts/blob/edit_blob.js.coffee @@ -6,6 +6,7 @@ class @EditBlob ace_mode = mode editor = ace.edit("editor") editor.focus() + @editor = editor if ace_mode editor.getSession().setMode "ace/mode/" + ace_mode @@ -38,3 +39,6 @@ class @EditBlob currentPane.fadeIn 200 editor.focus() return + + editor: -> + return @editor diff --git a/app/assets/javascripts/blob/new_blob.js.coffee b/app/assets/javascripts/blob/new_blob.js.coffee index ed4b7c4793..a6e27116b4 100644 --- a/app/assets/javascripts/blob/new_blob.js.coffee +++ b/app/assets/javascripts/blob/new_blob.js.coffee @@ -6,6 +6,7 @@ class @NewBlob ace_mode = mode editor = ace.edit("editor") editor.focus() + @editor = editor if ace_mode editor.getSession().setMode "ace/mode/" + ace_mode @@ -15,3 +16,6 @@ class @NewBlob $("#file-content").val editor.getValue() $(".file-editor form").submit() return + + editor: -> + return @editor diff --git a/app/assets/stylesheets/sections/editor.scss b/app/assets/stylesheets/sections/editor.scss index becd593331..88aa256e56 100644 --- a/app/assets/stylesheets/sections/editor.scss +++ b/app/assets/stylesheets/sections/editor.scss @@ -35,15 +35,15 @@ .file-title { @extend .monospace; font-size: 14px; + padding: 5px; } .editor-ref { - background: #fafafa; - padding: 18px 15px; - padding-left: 25px; + background: #f5f5f5; + padding: 11px 15px; border-right: 1px solid #CCC; display: inline-block; - margin: -10px -15px; + margin: -5px -5px; margin-right: 10px; } diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index c0734d9f47..b150b63988 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -21,4 +21,4 @@ cancel_path: @after_edit_path :javascript - new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}") + blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}") diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 70f52332cc..df6aedbe17 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -9,4 +9,4 @@ cancel_path: project_tree_path(@project, @id) :javascript - new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null) + blob = new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null) diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 1caad73654..bd1ca55a20 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -58,7 +58,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps step 'I can edit code' do set_new_content - evaluate_script('editor.getValue()').should == new_gitignore_content + evaluate_script('blob.editor.getValue()').should == new_gitignore_content end step 'I edit code' do @@ -103,7 +103,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps step 'I can see new file page' do page.should have_content "New file" - page.should have_content "File name" page.should have_content "Commit message" end @@ -170,7 +169,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps private def set_new_content - execute_script("editor.setValue('#{new_gitignore_content}')") + execute_script("blob.editor.setValue('#{new_gitignore_content}')") end # Content of the gitignore file on the seed repository. From ededa98995208591c5792c0ece1a5ec0ef302127 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Mon, 26 Jan 2015 22:46:54 -0800 Subject: [PATCH 091/124] Shorter check in services controller. --- app/controllers/projects/services_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index c7cc38b9c6..09bccb4bf8 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -17,7 +17,7 @@ class Projects::ServicesController < Projects::ApplicationController def update if @service.update_attributes(service_params) - if @service.activated? && @service.category == :issue_tracker + if @service.activated? && @service.issue_tracker? @project.update_attributes(issues_tracker: @service.to_param) end redirect_to edit_project_service_path(@project, @service.to_param), From 93bc2d5202e5802bd31419d05232b62355516a53 Mon Sep 17 00:00:00 2001 From: Boyan Tabakov Date: Thu, 21 Aug 2014 13:53:32 +0300 Subject: [PATCH 092/124] Added support for firing system hooks on group create/destroy and adding/removing users to group. Added tests and updated docs. Also adding 'user_id' field in the hooks for adding/removing user from team. --- CHANGELOG | 1 + app/models/group.rb | 15 ++++++ app/models/members/group_member.rb | 14 ++++- app/services/system_hooks_service.rb | 23 ++++++++ doc/system_hooks/system_hooks.md | 63 +++++++++++++++++++++- spec/models/system_hook_spec.rb | 35 ++++++++++++ spec/services/system_hooks_service_spec.rb | 31 +++++++++++ 7 files changed, 179 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index dd9b13ceac..975cb0af8c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -59,6 +59,7 @@ v 7.8.0 - - - + - Added support for firing system hooks on group create/destroy and adding/removing users to group (Boyan Tabakov) v 7.7.1 - Improve mention autocomplete performance diff --git a/app/models/group.rb b/app/models/group.rb index 733afa2fc0..e098dfb3cd 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -25,6 +25,9 @@ class Group < Namespace mount_uploader :avatar, AttachmentUploader + after_create :post_create_hook + after_destroy :post_destroy_hook + def human_name name end @@ -74,6 +77,18 @@ class Group < Namespace projects.public_only.any? end + def post_create_hook + system_hook_service.execute_hooks_for(self, :create) + end + + def post_destroy_hook + system_hook_service.execute_hooks_for(self, :destroy) + end + + def system_hook_service + SystemHooksService.new + end + class << self def search(query) where("LOWER(namespaces.name) LIKE :query", query: "%#{query.downcase}%") diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index b7f296b13f..28d0b4483b 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -27,8 +27,9 @@ class GroupMember < Member scope :with_group, ->(group) { where(source_id: group.id) } scope :with_user, ->(user) { where(user_id: user.id) } - after_create :notify_create + after_create :post_create_hook after_update :notify_update + after_destroy :post_destroy_hook def self.access_level_roles Gitlab::Access.options_with_owner @@ -42,8 +43,9 @@ class GroupMember < Member access_level end - def notify_create + def post_create_hook notification_service.new_group_member(self) + system_hook_service.execute_hooks_for(self, :create) end def notify_update @@ -52,6 +54,14 @@ class GroupMember < Member end end + def post_destroy_hook + system_hook_service.execute_hooks_for(self, :destroy) + end + + def system_hook_service + SystemHooksService.new + end + def notification_service NotificationService.new end diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index 44e494525b..46f6e91e80 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -60,6 +60,26 @@ class SystemHooksService access_level: model.human_access, project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase }) + when Group + owner = model.owner + + data.merge!( + name: model.name, + path: model.path, + group_id: model.id, + owner_name: owner.respond_to?(:name) ? owner.name : nil, + owner_email: owner.respond_to?(:email) ? owner.email : nil, + ) + when GroupMember + data.merge!( + group_name: model.group.name, + group_path: model.group.path, + group_id: model.group.id, + user_name: model.user.name, + user_email: model.user.email, + user_id: model.user.id, + group_access: model.human_access, + ) end end @@ -68,6 +88,9 @@ class SystemHooksService when ProjectMember return "user_add_to_team" if event == :create return "user_remove_from_team" if event == :destroy + when GroupMember + return 'user_add_to_group' if event == :create + return 'user_remove_from_group' if event == :destroy else "#{model.class.name.downcase}_#{event.to_s}" end diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index 54e6e3a9e3..41c2732ef7 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -1,6 +1,6 @@ # System hooks -Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create` and `key_destroy`. +Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`. System hooks can be used, e.g. for logging or changing information in a LDAP server. @@ -50,6 +50,7 @@ System hooks can be used, e.g. for logging or changing information in a LDAP ser "project_path": "storecloud", "user_email": "johnsmith@gmail.com", "user_name": "John Smith", + "user_id": 41, "project_visibility": "private", } ``` @@ -66,6 +67,7 @@ System hooks can be used, e.g. for logging or changing information in a LDAP ser "project_path": "storecloud", "user_email": "johnsmith@gmail.com", "user_name": "John Smith", + "user_id": 41, "project_visibility": "private", } ``` @@ -117,3 +119,62 @@ System hooks can be used, e.g. for logging or changing information in a LDAP ser "id": 4 } ``` + +**Group created:** + +```json +{ + "created_at": "2012-07-21T07:30:54Z", + "event_name": "group_create", + "name": "StormCloud", + "owner_email": "johnsmith@gmail.com", + "owner_name": "John Smith", + "path": "stormcloud", + "group_id": 78 +} +``` + +**Group removed:** + +```json +{ + "created_at": "2012-07-21T07:30:54Z", + "event_name": "group_destroy", + "name": "StoreCloud", + "owner_email": "johnsmith@gmail.com", + "owner_name": "John Smith", + "path": "storecloud", + "group_id": 78 +} +``` + +**New Group Member:** + +```json +{ + "created_at": "2012-07-21T07:30:56Z", + "event_name": "user_add_to_group", + "group_access": "Master", + "group_id": 78, + "group_name": "StoreCloud", + "group_path": "storecloud", + "user_email": "johnsmith@gmail.com", + "user_name": "John Smith", + "user_id": 41 +} +``` +**Group Member Removed:** + +```json +{ + "created_at": "2012-07-21T07:30:56Z", + "event_name": "user_remove_from_group", + "group_access": "Master", + "group_id": 78, + "group_name": "StoreCloud", + "group_path": "storecloud", + "user_email": "johnsmith@gmail.com", + "user_name": "John Smith", + "user_id": 41 +} +``` diff --git a/spec/models/system_hook_spec.rb b/spec/models/system_hook_spec.rb index 4ab5261dc9..8deb732de9 100644 --- a/spec/models/system_hook_spec.rb +++ b/spec/models/system_hook_spec.rb @@ -61,5 +61,40 @@ describe SystemHook do project.project_members.destroy_all WebMock.should have_requested(:post, @system_hook.url).with(body: /user_remove_from_team/).once end + + it 'group create hook' do + create(:group) + WebMock.should have_requested(:post, @system_hook.url).with( + body: /group_create/ + ).once + end + + it 'group destroy hook' do + group = create(:group) + group.destroy + WebMock.should have_requested(:post, @system_hook.url).with( + body: /group_destroy/ + ).once + end + + it 'group member create hook' do + group = create(:group) + user = create(:user) + group.add_user(user, Gitlab::Access::MASTER) + WebMock.should have_requested(:post, @system_hook.url).with( + body: /user_add_to_group/ + ).once + end + + it 'group member destroy hook' do + group = create(:group) + user = create(:user) + group.add_user(user, Gitlab::Access::MASTER) + group.group_members.destroy_all + WebMock.should have_requested(:post, @system_hook.url).with( + body: /user_remove_from_group/ + ).once + end + end end diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index 573446d3a1..a45e9d0575 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -5,6 +5,8 @@ describe SystemHooksService do let (:project) { create :project } let (:project_member) { create :project_member } let (:key) { create(:key, user: user) } + let (:group) { create(:group) } + let (:group_member) { create(:group_member) } context 'event data' do it { event_data(user, :create).should include(:event_name, :name, :created_at, :email, :user_id) } @@ -15,6 +17,31 @@ describe SystemHooksService do it { event_data(project_member, :destroy).should include(:event_name, :created_at, :project_name, :project_path, :project_id, :user_name, :user_email, :access_level, :project_visibility) } it { event_data(key, :create).should include(:username, :key, :id) } it { event_data(key, :destroy).should include(:username, :key, :id) } + + it do + event_data(group, :create).should include( + :event_name, :name, :created_at, :path, :group_id, :owner_name, + :owner_email + ) + end + it do + event_data(group, :destroy).should include( + :event_name, :name, :created_at, :path, :group_id, :owner_name, + :owner_email + ) + end + it do + event_data(group_member, :create).should include( + :event_name, :created_at, :group_name, :group_path, :group_id, :user_id, + :user_name, :user_email, :group_access + ) + end + it do + event_data(group_member, :destroy).should include( + :event_name, :created_at, :group_name, :group_path, :group_id, :user_id, + :user_name, :user_email, :group_access + ) + end end context 'event names' do @@ -26,6 +53,10 @@ describe SystemHooksService do it { event_name(project_member, :destroy).should eq "user_remove_from_team" } it { event_name(key, :create).should eq 'key_create' } it { event_name(key, :destroy).should eq 'key_destroy' } + it { event_name(group, :create).should eq 'group_create' } + it { event_name(group, :destroy).should eq 'group_destroy' } + it { event_name(group_member, :create).should eq 'user_add_to_group' } + it { event_name(group_member, :destroy).should eq 'user_remove_from_group' } end def event_data(*args) From 4fefd353d86a0b7b223b44c5f2e355b09231f79d Mon Sep 17 00:00:00 2001 From: yglukhov Date: Tue, 27 Jan 2015 15:14:52 +0200 Subject: [PATCH 093/124] Update semantic-ui-sass to 1.8 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 96a1097d6d..a49e691298 100644 --- a/Gemfile +++ b/Gemfile @@ -170,7 +170,7 @@ gem 'ace-rails-ap' gem 'mousetrap-rails' # Semantic UI Sass for Sidebar -gem 'semantic-ui-sass', '~> 0.16.1.0' +gem 'semantic-ui-sass', '~> 1.8.0' gem "sass-rails", '~> 4.0.2' gem "coffee-rails" diff --git a/Gemfile.lock b/Gemfile.lock index 18fae9b700..69ed6af2c6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -490,7 +490,7 @@ GEM activesupport (>= 3.1, < 4.2) select2-rails (3.5.2) thor (~> 0.14) - semantic-ui-sass (0.16.1.0) + semantic-ui-sass (1.8.0.0) sass (~> 3.2) settingslogic (2.0.9) sexp_processor (4.4.0) @@ -715,7 +715,7 @@ DEPENDENCIES sdoc seed-fu select2-rails - semantic-ui-sass (~> 0.16.1.0) + semantic-ui-sass (~> 1.8.0) settingslogic shoulda-matchers (~> 2.1.0) sidekiq (~> 3.3) From 7e6fa0afd63b75d2653ff75fcde26164f832a094 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 27 Jan 2015 09:30:16 -0800 Subject: [PATCH 094/124] Update CHANGELOG with new stuff --- CHANGELOG | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 91409707c8..56d3ac092e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,8 +5,8 @@ v 7.8.0 - Make project search case insensitive (Hannes Rosenögger) - Include issue/mr participants in list of recipients for reassign/close/reopen emails - Expose description in groups API - - - - + - Better UI for project services page + - Cleaner UI for web editor - Add diff syntax highlighting in email-on-push service notifications (Hannes Rosenögger) - - From 233f9f0766f4316661f9464698d971069ea30e37 Mon Sep 17 00:00:00 2001 From: Sytse Sijbrandij Date: Tue, 27 Jan 2015 09:35:46 -0800 Subject: [PATCH 095/124] Prevent confusion between active users and 30 day users. --- app/views/admin/dashboard/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index dd95af426c..32e0e4a684 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -32,7 +32,7 @@ %span.light.pull-right = Milestone.count %p - Active users last 30 days + Users who signed in during last 30 days %span.light.pull-right = User.where("current_sign_in_at > ?", 30.days.ago).count .col-md-4 From 6182f1cabf08e75b01146198cbd09fdea0bfdb67 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 27 Jan 2015 09:48:42 -0800 Subject: [PATCH 096/124] Use larger avatar on application header --- app/assets/stylesheets/sections/header.scss | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/sections/header.scss b/app/assets/stylesheets/sections/header.scss index 047617e54b..e255cbcada 100644 --- a/app/assets/stylesheets/sections/header.scss +++ b/app/assets/stylesheets/sections/header.scss @@ -138,9 +138,10 @@ header { top: -1px; padding-right: 0px !important; img { - width: 26px; - height: 26px; - @include border-radius($avatar_radius); + width: 50px; + height: 50px; + margin: -15px; + margin-left: 5px; } } From 2fa36ddd9c9efcc4f0d40755f535867573a0483c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 27 Jan 2015 11:32:28 -0800 Subject: [PATCH 097/124] Replace p with h4 for empty repo text --- app/views/projects/empty.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 776a7327bc..36628195b4 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -6,7 +6,7 @@ .center.well %h3 The repository for this project is empty - %p.lead + %h4 You can = link_to project_new_blob_path(@project, 'master'), class: 'btn btn-new btn-lg' do add a file From 95db00c3e9b7658b1a2a38f62006371988eabe5c Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Tue, 27 Jan 2015 12:19:32 -0800 Subject: [PATCH 098/124] Still need the javascript on the project edit page. --- app/assets/javascripts/dispatcher.js.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index acce4ad509..1643ca941f 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -100,6 +100,7 @@ class Dispatcher switch path[1] when 'edit' shortcut_handler = new ShortcutsNavigation() + new ProjectNew() when 'new' new ProjectNew() when 'show' From e956066d4a9c3f556a52f80acfb5c761aede7c6c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 27 Jan 2015 12:19:32 -0800 Subject: [PATCH 099/124] Add tests for initializing bare repo and creating new file in it --- features/project/source/browse_files.feature | 13 +++++++++++++ features/steps/project/source/browse_files.rb | 11 +++++++++++ features/steps/shared/project.rb | 4 ++++ 3 files changed, 28 insertions(+) diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index 6ea64f7009..ccb29293a8 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -34,6 +34,19 @@ Feature: Project Source Browse Files Then I am redirected to the new file And I should see its new content + @javascript + Scenario: I can create file in empty repo + Given I own an empty project + And I visit my empty project page + And I create bare repo + When I click on "add a file" link + And I edit code + And I fill the new file name + And I fill the commit message + And I click on "Commit Changes" + Then I am redirected to the new file + And I should see its new content + @javascript Scenario: If I enter an illegal file name I see an error message Given I click on "new file" link in repo diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index bd1ca55a20..770e816249 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -166,6 +166,17 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps expect(page).to have_content('Your changes could not be committed') end + step 'I create bare repo' do + click_link 'Create empty bare repository' + end + + step 'I click on "add a file" link' do + click_link 'add a file' + + # Remove pre-receive hook so we can push without auth + FileUtils.rm(File.join(Project.last.repository.path, 'hooks', 'pre-receive')) + end + private def set_new_content diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index 0bd5653538..cf0be25623 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -28,6 +28,10 @@ module SharedProject @project.team << [@user, :master] end + step 'I visit my empty project page' do + visit project_path(Project.find_by(name: 'Empty Project')) + end + step 'project "Shop" has push event' do @project = Project.find_by(name: "Shop") From b35f1d1c2f8741e777a670e06785b6ff47e7e764 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 27 Jan 2015 12:45:31 -0800 Subject: [PATCH 100/124] Increase font size for issue/mr titles --- app/assets/stylesheets/sections/issues.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/sections/issues.scss b/app/assets/stylesheets/sections/issues.scss index fbfd9c8cd9..7a9d3334d9 100644 --- a/app/assets/stylesheets/sections/issues.scss +++ b/app/assets/stylesheets/sections/issues.scss @@ -163,8 +163,9 @@ form.edit-issue { } } -.issue-title { +h3.issue-title { margin-top: 0; + font-size: 2em; } .context .select2-container { From 8cc111bd038e4c4d67b835de4a1996c04c6674d6 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 27 Jan 2015 13:40:23 -0800 Subject: [PATCH 101/124] Fix random failing test --- spec/features/atom/dashboard_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb index a7f87906b2..52ade3e2d3 100644 --- a/spec/features/atom/dashboard_spec.rb +++ b/spec/features/atom/dashboard_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe "Dashboard Feed", feature: true do describe "GET /" do - let!(:user) { create(:user) } + let!(:user) { create(:user, name: "Jonh") } context "projects atom feed via private token" do it "should render projects atom feed" do From 35d6d1ce4669d1f3850862f3c144abaaf5b841d3 Mon Sep 17 00:00:00 2001 From: Visay Keo Date: Mon, 26 Jan 2015 12:12:16 +0700 Subject: [PATCH 102/124] Fix broadcast message to show up properly with new UI With the new UI, the broadcast message is the first level child element of the body tag and then render full width without respecting the width of the left sidebar. This makes the message goes under the left sidebar in smaller screen. This commit fixes the issue by moving the message element into the "page-with-sidebar" div so it will always render together with the main content area with same look as before. The rendering for the search, login and other view without left sidebar remains untouched. Releases: master, 7-7-stable Fixes: #1019 --- CHANGELOG | 2 +- app/views/layouts/_page.html.haml | 1 + app/views/layouts/admin.html.haml | 1 - app/views/layouts/application.html.haml | 1 - app/views/layouts/group.html.haml | 1 - app/views/layouts/profile.html.haml | 1 - app/views/layouts/project_settings.html.haml | 1 - app/views/layouts/projects.html.haml | 1 - app/views/layouts/public_group.html.haml | 1 - app/views/layouts/public_projects.html.haml | 1 - app/views/layouts/public_users.html.haml | 1 - 11 files changed, 2 insertions(+), 10 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b05f0e760b..999b21f758 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -33,7 +33,7 @@ v 7.8.0 - - - - - + - Fix long broadcast message cut-off on left sidebar (Visay Keo) - Add Project Avatars (Steven Thonus and Hannes Rosenögger) - - diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 621365fa6a..1263f44eca 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,5 +1,6 @@ - if defined?(sidebar) .page-with-sidebar + = render "layouts/broadcast" .sidebar-wrapper = render(sidebar) .content-wrapper diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index fb62d5fea0..dc8652cb14 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -2,6 +2,5 @@ %html{ lang: "en"} = render "layouts/head", title: "Admin area" %body{class: "#{app_theme} #{theme_type} admin", :'data-page' => body_data_page} - = render "layouts/broadcast" = render "layouts/head_panel", title: "Admin area" = render 'layouts/page', sidebar: 'layouts/nav/admin' diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index d40c9753b1..e5420a1360 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -2,6 +2,5 @@ %html{ lang: "en"} = render "layouts/head", title: "Dashboard" %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page } - = render "layouts/broadcast" = render "layouts/head_panel", title: "Dashboard" = render 'layouts/page', sidebar: 'layouts/nav/dashboard' diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index 72b0d03908..98edcf3a14 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -2,6 +2,5 @@ %html{ lang: "en"} = render "layouts/head", title: group_head_title %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} - = render "layouts/broadcast" = render "layouts/head_panel", title: @group.name = render 'layouts/page', sidebar: 'layouts/nav/group' diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index 941084cc4a..89d816061e 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -2,6 +2,5 @@ %html{ lang: "en"} = render "layouts/head", title: "Profile" %body{class: "#{app_theme} #{theme_type} profile", :'data-page' => body_data_page} - = render "layouts/broadcast" = render "layouts/head_panel", title: "Profile" = render 'layouts/page', sidebar: 'layouts/nav/profile' diff --git a/app/views/layouts/project_settings.html.haml b/app/views/layouts/project_settings.html.haml index 0f20bf38bf..d2c9c2a991 100644 --- a/app/views/layouts/project_settings.html.haml +++ b/app/views/layouts/project_settings.html.haml @@ -2,7 +2,6 @@ %html{ lang: "en"} = render "layouts/head", title: @project.name_with_namespace %body{class: "#{app_theme} #{theme_type} project", :'data-page' => body_data_page, :'data-project-id' => @project.id } - = render "layouts/broadcast" = render "layouts/head_panel", title: project_title(@project) = render "layouts/init_auto_complete" - @project_settings_nav = true diff --git a/app/views/layouts/projects.html.haml b/app/views/layouts/projects.html.haml index d4ee53db55..c44a40c9c1 100644 --- a/app/views/layouts/projects.html.haml +++ b/app/views/layouts/projects.html.haml @@ -2,7 +2,6 @@ %html{ lang: "en"} = render "layouts/head", title: project_head_title %body{class: "#{app_theme} #{theme_type} project", :'data-page' => body_data_page, :'data-project-id' => @project.id } - = render "layouts/broadcast" = render "layouts/head_panel", title: project_title(@project) = render "layouts/init_auto_complete" = render 'layouts/page', sidebar: 'layouts/nav/project' diff --git a/app/views/layouts/public_group.html.haml b/app/views/layouts/public_group.html.haml index 64794104ac..ae3d2bd8a8 100644 --- a/app/views/layouts/public_group.html.haml +++ b/app/views/layouts/public_group.html.haml @@ -2,6 +2,5 @@ %html{ lang: "en"} = render "layouts/head", title: group_head_title %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} - = render "layouts/broadcast" = render "layouts/public_head_panel", title: "group: #{@group.name}" = render 'layouts/page', sidebar: 'layouts/nav/group' diff --git a/app/views/layouts/public_projects.html.haml b/app/views/layouts/public_projects.html.haml index 5964a29d52..027e9a5313 100644 --- a/app/views/layouts/public_projects.html.haml +++ b/app/views/layouts/public_projects.html.haml @@ -2,6 +2,5 @@ %html{ lang: "en"} = render "layouts/head", title: @project.name_with_namespace %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} - = render "layouts/broadcast" = render "layouts/public_head_panel", title: project_title(@project) = render 'layouts/page', sidebar: 'layouts/nav/project' diff --git a/app/views/layouts/public_users.html.haml b/app/views/layouts/public_users.html.haml index 0510ce34a7..37767df33d 100644 --- a/app/views/layouts/public_users.html.haml +++ b/app/views/layouts/public_users.html.haml @@ -2,6 +2,5 @@ %html{ lang: "en"} = render "layouts/head", title: @title %body{class: "#{app_theme} #{theme_type} application", :'data-page' => body_data_page} - = render "layouts/broadcast" = render "layouts/public_head_panel", title: @title = render 'layouts/page' From 50297f7134e878fba84168785ae461d14e08c572 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 27 Jan 2015 22:51:11 -0800 Subject: [PATCH 103/124] Fix gravatar size for head panel icon --- app/views/layouts/_head_panel.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml index bdf27562c2..77bfe4f996 100644 --- a/app/views/layouts/_head_panel.html.haml +++ b/app/views/layouts/_head_panel.html.haml @@ -43,6 +43,6 @@ %i.fa.fa-sign-out %li.hidden-xs = link_to current_user, class: "profile-pic", id: 'profile-pic' do - = image_tag avatar_icon(current_user.email, 26), alt: 'User activity' + = image_tag avatar_icon(current_user.email, 60), alt: 'User activity' = render 'shared/outdated_browser' From 8eb365c0a04b912d2e10eb16adeeb4216563be2c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Tue, 27 Jan 2015 22:54:46 -0800 Subject: [PATCH 104/124] Separate admin settings from other links --- app/views/layouts/nav/_admin.html.haml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index 66770adb5a..4813a4f16f 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -40,14 +40,15 @@ %span Background Jobs - = nav_link(controller: :application_settings) do - = link_to admin_application_settings_path, title: 'Settings' do - %i.fa.fa-cogs - %span - Settings - = nav_link(controller: :applications) do = link_to admin_applications_path, title: 'Applications' do %i.fa.fa-cloud %span Applications + + = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do + = link_to admin_application_settings_path, title: 'Settings' do + %i.fa.fa-cogs + %span + Settings + From 43da3f0929521615fe77d1dcb85318a8128bb7e8 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 28 Jan 2015 11:09:11 +0100 Subject: [PATCH 105/124] Point out common LDAP port/method combinations --- doc/integration/ldap.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index 56b0d826ad..6172a61d00 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -76,6 +76,9 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server EOS ``` +If you are getting 'Connection Refused' errors when trying to connect to the LDAP server please double-check the LDAP `port` and `method` settings used by GitLab. +Common combinations are `method: 'plain'` and `port: 389`, OR `method: 'ssl'` and `port: 636`. + If you are using a GitLab installation from source you can find the LDAP settings in `/home/git/gitlab/config/gitlab.yml`: ``` From 087c4cbc3cba2cce1c25773af304833d217976fc Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 28 Jan 2015 11:08:44 +0100 Subject: [PATCH 106/124] Make 'plain', port 389 the default for LDAP --- config/gitlab.yml.example | 4 ++-- doc/integration/ldap.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index e5780cabb6..59af49c018 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -153,9 +153,9 @@ production: &base label: 'LDAP' host: '_your_ldap_server' - port: 636 + port: 389 uid: 'sAMAccountName' - method: 'ssl' # "tls" or "ssl" or "plain" + method: 'plain' # "tls" or "ssl" or "plain" bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' password: '_the_password_of_the_bind_user' diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md index 6172a61d00..125ce31b52 100644 --- a/doc/integration/ldap.md +++ b/doc/integration/ldap.md @@ -29,9 +29,9 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server label: 'LDAP' host: '_your_ldap_server' - port: 636 + port: 389 uid: 'sAMAccountName' - method: 'ssl' # "tls" or "ssl" or "plain" + method: 'plain' # "tls" or "ssl" or "plain" bind_dn: '_the_full_dn_of_the_user_you_will_bind_with' password: '_the_password_of_the_bind_user' From 537cd66d7e4237f0df6db88b3225327c8e4140c5 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 28 Jan 2015 09:28:17 -0800 Subject: [PATCH 107/124] Add gitlab internal issue tracker service. --- app/controllers/application_controller.rb | 2 +- .../projects/services_controller.rb | 5 +--- app/models/project.rb | 24 ++++++++++++++----- .../gitlab_issue_tracker_service.rb | 13 ++++++++++ .../project_services/issue_tracker_service.rb | 4 ++++ 5 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 app/models/project_services/gitlab_issue_tracker_service.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ad13a0ac3e..36e1370676 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -181,7 +181,7 @@ class ApplicationController < ActionController::Base end def add_gon_variables - gon.default_issues_tracker = Project.issues_tracker.default_value + gon.default_issues_tracker = Project.new.default_issue_tracker.to_param gon.api_version = API::API.version gon.relative_url_root = Gitlab.config.gitlab.relative_url_root gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 09bccb4bf8..5dda869a15 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -9,7 +9,7 @@ class Projects::ServicesController < Projects::ApplicationController def index @project.build_missing_services - @services = @project.services.reload + @services = @project.services.where.not(type: 'GitlabIssueTrackerService').reload end def edit @@ -17,9 +17,6 @@ class Projects::ServicesController < Projects::ApplicationController def update if @service.update_attributes(service_params) - if @service.activated? && @service.issue_tracker? - @project.update_attributes(issues_tracker: @service.to_param) - end redirect_to edit_project_service_path(@project, @service.to_param), notice: 'Successfully updated.' else diff --git a/app/models/project.rb b/app/models/project.rb index 12751bb77e..9e31c019fd 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -77,6 +77,7 @@ class Project < ActiveRecord::Base has_one :jira_service, dependent: :destroy has_one :redmine_service, dependent: :destroy has_one :custom_issue_tracker_service, dependent: :destroy + has_one :gitlab_issue_tracker_service, dependent: :destroy has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" @@ -149,8 +150,6 @@ class Project < ActiveRecord::Base scope :public_and_internal_only, -> { where(visibility_level: Project.public_and_internal_levels) } scope :non_archived, -> { where(archived: false) } - enumerize :issues_tracker, in: (Service.issue_tracker_service_list).append(:gitlab), default: :gitlab - state_machine :import_status, initial: :none do event :import_start do transition [:none, :finished] => :started @@ -317,19 +316,32 @@ class Project < ActiveRecord::Base end end + def default_issue_tracker + unless gitlab_issue_tracker_service + create_gitlab_issue_tracker_service + end + + gitlab_issue_tracker_service + end + + def issues_tracker + if external_issue_tracker + external_issue_tracker + else + default_issue_tracker + end + end + def default_issues_tracker? if external_issue_tracker false else - unless self.issues_tracker == Project.issues_tracker.default_value - self.update_attributes(issues_tracker: Project.issues_tracker.default_value) - end true end end def external_issues_trackers - services.select { |service| service.issue_tracker? } + services.select(&:issue_tracker?).reject(&:default?) end def external_issue_tracker diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb new file mode 100644 index 0000000000..46649a7475 --- /dev/null +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -0,0 +1,13 @@ +class GitlabIssueTrackerService < IssueTrackerService + + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + + + def default? + true + end + + def to_param + 'gitlab' + end +end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index fc7b2fe5ac..810ecbe46f 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -6,6 +6,10 @@ class IssueTrackerService < Service :issue_tracker end + def default? + false + end + def project_url # implement inside child end From 965cec68accf9e9c2137e433ab00283f2ae6987f Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 28 Jan 2015 10:53:28 -0800 Subject: [PATCH 108/124] Project/Group access dropdown should contain link to permissions help page --- app/views/groups/_new_group_member.html.haml | 6 +++++- app/views/projects/team_members/_form.html.haml | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/views/groups/_new_group_member.html.haml b/app/views/groups/_new_group_member.html.haml index e590ddbf93..ed00153de7 100644 --- a/app/views/groups/_new_group_member.html.haml +++ b/app/views/groups/_new_group_member.html.haml @@ -5,7 +5,11 @@ .form-group = f.label :access_level, "Group Access", class: 'control-label' - .col-sm-10= select_tag :access_level, options_for_select(GroupMember.access_level_roles, @users_group.access_level), class: "project-access-select select2" + .col-sm-10 + = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @users_group.access_level), class: "project-access-select select2" + .help-block + Read more about role permissions + %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" .form-actions = f.submit 'Add users into group', class: "btn btn-create" diff --git a/app/views/projects/team_members/_form.html.haml b/app/views/projects/team_members/_form.html.haml index 2bf61fa12b..ddf8cb76f7 100644 --- a/app/views/projects/team_members/_form.html.haml +++ b/app/views/projects/team_members/_form.html.haml @@ -17,7 +17,12 @@ %p 2. Set access level for them .form-group = f.label :access_level, "Project Access", class: 'control-label' - .col-sm-10= select_tag :access_level, options_for_select(Gitlab::Access.options, @user_project_relation.access_level), class: "project-access-select select2" + .col-sm-10 + = select_tag :access_level, options_for_select(Gitlab::Access.options, @user_project_relation.access_level), class: "project-access-select select2" + .help-block + Read more about role permissions + %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" + .form-actions = f.submit 'Add users', class: "btn btn-create" From b40809d73135ada0e82c826f96e6cb1dd6fbaa7c Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 28 Jan 2015 11:25:13 -0800 Subject: [PATCH 109/124] Improve UX for widget if merge request can not be merged --- app/assets/stylesheets/sections/merge_requests.scss | 1 + .../projects/merge_requests/show/_mr_accept.html.haml | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/sections/merge_requests.scss b/app/assets/stylesheets/sections/merge_requests.scss index 8bd32f41e2..0e27c38938 100644 --- a/app/assets/stylesheets/sections/merge_requests.scss +++ b/app/assets/stylesheets/sections/merge_requests.scss @@ -122,6 +122,7 @@ background: $box_bg; margin-bottom: 20px; color: #666; + border: 1px solid #EEE; @include box-shadow(0 1px 1px rgba(0, 0, 0, 0.09)); .ci_widget { diff --git a/app/views/projects/merge_requests/show/_mr_accept.html.haml b/app/views/projects/merge_requests/show/_mr_accept.html.haml index 11a111e5fa..f8ee697363 100644 --- a/app/views/projects/merge_requests/show/_mr_accept.html.haml +++ b/app/views/projects/merge_requests/show/_mr_accept.html.haml @@ -45,10 +45,17 @@ .automerge_widget.cannot_be_merged.hide %h4 This request can't be merged with GitLab. - %p You should do it manually with %strong - = link_to "command line", "#modal_merge_info", class: "how_to_merge_link", title: "How To Merge", "data-toggle" => "modal" + = link_to "#modal_merge_info", class: "underlined-link how_to_merge_link", title: "How To Merge", "data-toggle" => "modal" do + command line + + %p + %button.btn.disabled + %i.fa.fa-warning + Accept Merge Request +   + This usually happens when git can not resolve conflicts between branches automatically. .automerge_widget.unchecked %p From c6c7552e41cba12ca84238bd466d522aa1712220 Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 28 Jan 2015 13:19:32 -0800 Subject: [PATCH 110/124] Build the urls inside of the service. --- app/helpers/issues_helper.rb | 19 +++---------------- .../gitlab_issue_tracker_service.rb | 14 +++++++++++++- .../project_services/issue_tracker_service.rb | 4 ++++ spec/helpers/gitlab_markdown_helper_spec.rb | 1 + 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 2bf430f914..9fe183e6e2 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -16,32 +16,19 @@ module IssuesHelper def url_for_project_issues(project = @project) return '' if project.nil? - if project.default_issues_tracker? - project_issues_path(project) - else - project.external_issue_tracker.project_url - end + project.issues_tracker.project_url end def url_for_new_issue(project = @project) return '' if project.nil? - if project.default_issues_tracker? - url = new_project_issue_path project_id: project - else - project.external_issue_tracker.new_issue_url - end + project.issues_tracker.new_issue_url end def url_for_issue(issue_iid, project = @project) return '' if project.nil? - if project.default_issues_tracker? - url = project_issue_url project_id: project, id: issue_iid - else - url = project.external_issue_tracker.issues_url - url.gsub(':id', issue_iid.to_s) - end + project.issues_tracker.issue_url(issue_iid) end def title_for_issue(issue_iid, project = @project) diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 46649a7475..8e548a6d63 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -1,5 +1,5 @@ class GitlabIssueTrackerService < IssueTrackerService - + include Rails.application.routes.url_helpers prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url @@ -10,4 +10,16 @@ class GitlabIssueTrackerService < IssueTrackerService def to_param 'gitlab' end + + def project_url + project_issues_path(project) + end + + def new_issue_url + new_project_issue_path project_id: project + end + + def issue_url(iid) + "#{Gitlab.config.gitlab.url}#{project_issue_path project_id: project, id: iid}" + end end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 810ecbe46f..632f053d17 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -22,6 +22,10 @@ class IssueTrackerService < Service # implement inside child end + def issue_url(iid) + self.issues_url.gsub(':id', iid.to_s) + end + def fields [ { type: 'text', name: 'description', placeholder: description }, diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 5c9eea956f..d633287b2a 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -23,6 +23,7 @@ describe GitlabMarkdownHelper do @project = project @ref = 'markdown' @repository = project.repository + @request.host = Gitlab.config.gitlab.host end describe "#gfm" do From 34b7472b33986db8894939b59116784f706d4759 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 28 Jan 2015 14:20:00 -0800 Subject: [PATCH 111/124] Improve wording for fork project page --- app/views/projects/forks/new.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml index 54f2cef023..13f3a4b2e8 100644 --- a/app/views/projects/forks/new.html.haml +++ b/app/views/projects/forks/new.html.haml @@ -1,5 +1,6 @@ %h3.page-title Fork project -%p.lead Select namespace where to fork this project +%p.lead + Click on icon with user or group to fork project there %hr .fork-namespaces From 68f7302474768351abf12767c7741823f56f35cd Mon Sep 17 00:00:00 2001 From: Marin Jankovski Date: Wed, 28 Jan 2015 14:25:55 -0800 Subject: [PATCH 112/124] Add a scope for visible services, code styling changes for easier readability. --- app/controllers/projects/services_controller.rb | 2 +- app/models/project.rb | 6 +----- app/models/project_services/gitlab_issue_tracker_service.rb | 2 +- app/models/service.rb | 2 ++ 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 5dda869a15..5b35cc9041 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -9,7 +9,7 @@ class Projects::ServicesController < Projects::ApplicationController def index @project.build_missing_services - @services = @project.services.where.not(type: 'GitlabIssueTrackerService').reload + @services = @project.services.visible.reload end def edit diff --git a/app/models/project.rb b/app/models/project.rb index 9e31c019fd..b26c697a7b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -317,11 +317,7 @@ class Project < ActiveRecord::Base end def default_issue_tracker - unless gitlab_issue_tracker_service - create_gitlab_issue_tracker_service - end - - gitlab_issue_tracker_service + gitlab_issue_tracker_service ||= create_gitlab_issue_tracker_service end def issues_tracker diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 8e548a6d63..25f5f23bdf 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -20,6 +20,6 @@ class GitlabIssueTrackerService < IssueTrackerService end def issue_url(iid) - "#{Gitlab.config.gitlab.url}#{project_issue_path project_id: project, id: iid}" + "#{Gitlab.config.gitlab.url}#{project_issue_path(project_id: project, id: iid)}" end end diff --git a/app/models/service.rb b/app/models/service.rb index 4241947534..15948e63e4 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -26,6 +26,8 @@ class Service < ActiveRecord::Base validates :project_id, presence: true + scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } + def activated? active end From b076d8cddb37ea7ae24cd035967cba885cdc429d Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 28 Jan 2015 15:36:12 -0800 Subject: [PATCH 113/124] Fix test for merge request --- features/steps/project/merge_requests.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 071ef75dc6..6f421de1ab 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -173,7 +173,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps merge!: true, ) - click_button "Accept Merge Request" + within '.can_be_merged' do + click_button "Accept Merge Request" + end end step 'I should see merged request' do From f2f7b5a18e91005deda8ea36d95fd159568d93a4 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Wed, 28 Jan 2015 15:37:20 -0800 Subject: [PATCH 114/124] Better wording --- app/views/projects/forks/new.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml index 13f3a4b2e8..959d5f08d4 100644 --- a/app/views/projects/forks/new.html.haml +++ b/app/views/projects/forks/new.html.haml @@ -1,6 +1,6 @@ %h3.page-title Fork project %p.lead - Click on icon with user or group to fork project there + Click to fork the project to a user or group %hr .fork-namespaces From 54f6d8c7b5a1c67a222011c35ad70909da0e686d Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Tue, 27 Jan 2015 15:37:19 -0800 Subject: [PATCH 115/124] an ability to clone project with oauth2 token --- ...150116234545_add_gitlab_access_token_to_user.rb | 5 +++++ db/schema.rb | 3 ++- lib/gitlab/backend/grack_auth.rb | 14 +++++++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 db/migrate/20150116234545_add_gitlab_access_token_to_user.rb diff --git a/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb b/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb new file mode 100644 index 0000000000..c28ba3197a --- /dev/null +++ b/db/migrate/20150116234545_add_gitlab_access_token_to_user.rb @@ -0,0 +1,5 @@ +class AddGitlabAccessTokenToUser < ActiveRecord::Migration + def change + add_column :users, :gitlab_access_token, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 29466f048e..3f9ceb84e5 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: 20150116234544) do +ActiveRecord::Schema.define(version: 20150116234545) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -434,6 +434,7 @@ ActiveRecord::Schema.define(version: 20150116234544) do t.string "website_url", default: "", null: false t.datetime "last_credential_check_at" t.string "github_access_token" + t.string "gitlab_access_token" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 1f71906bc8..2e393f753e 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -34,7 +34,7 @@ module Grack def auth! if @auth.provided? return bad_request unless @auth.basic? - + # Authentication with username and password login, password = @auth.credentials @@ -71,8 +71,20 @@ module Grack false end + def oauth_access_token_check(login, password) + if login == "oauth2" && git_cmd == 'git-upload-pack' && password.present? + token = Doorkeeper::AccessToken.by_token(password) + token && token.accessible? && User.find_by(id: token.resource_owner_id) + end + end + def authenticate_user(login, password) user = Gitlab::Auth.new.find(login, password) + + unless user + user = oauth_access_token_check(login, password) + end + return user if user.present? # At this point, we know the credentials were wrong. We let Rack::Attack From d74e732299dc2c896a64350ddfdbdb2bb55a5fc6 Mon Sep 17 00:00:00 2001 From: Valery Sizov Date: Wed, 28 Jan 2015 23:53:16 +0000 Subject: [PATCH 116/124] update changelog --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 999b21f758..023d2f5449 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,8 @@ v 7.8.0 - Increate font size when browse source files and diffs - Create new file in empty repository using GitLab UI - + - Ability to clone project using oauth2 token + - - Upgrade Sidekiq gem to version 3.3.0 - Stop git zombie creation during force push check - Show success/error messages for test setting button in services From 792ced2f4190226c3335967a8e5a30d3b72bd4ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Wed, 28 Jan 2015 22:18:22 +0100 Subject: [PATCH 117/124] Add a commit calendar to the user profile --- CHANGELOG | 2 +- Gemfile | 3 + Gemfile.lock | 2 + app/assets/javascripts/application.js.coffee | 1 + app/assets/javascripts/calendar.js.coffee | 71 +++++++++++++++ app/assets/stylesheets/application.scss | 1 + app/assets/stylesheets/generic/calendar.scss | 95 ++++++++++++++++++++ app/controllers/users_controller.rb | 25 +++++- app/models/repository.rb | 35 ++++++-- app/views/events/_event.html.haml | 3 +- app/views/users/_calendar.html.haml | 9 ++ app/views/users/_calendar_onclick.html.haml | 25 ++++++ app/views/users/show.html.haml | 2 + config/routes.rb | 4 + lib/gitlab/commits_calendar.rb | 79 ++++++++++++++++ spec/controllers/users_controller_spec.rb | 27 ++++++ 16 files changed, 374 insertions(+), 10 deletions(-) create mode 100644 app/assets/javascripts/calendar.js.coffee create mode 100644 app/assets/stylesheets/generic/calendar.scss create mode 100644 app/views/users/_calendar.html.haml create mode 100644 app/views/users/_calendar_onclick.html.haml create mode 100644 lib/gitlab/commits_calendar.rb create mode 100644 spec/controllers/users_controller_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 999b21f758..8914068570 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -29,7 +29,7 @@ v 7.8.0 - - - - - + - Add a commit calendar to the user profile (Hannes Rosenögger) - - - diff --git a/Gemfile b/Gemfile index 96a1097d6d..6e4e20f5e1 100644 --- a/Gemfile +++ b/Gemfile @@ -154,6 +154,9 @@ gem "slack-notifier", "~> 1.0.0" # d3 gem "d3_rails", "~> 3.1.4" +#cal-heatmap +gem "cal-heatmap-rails", "~> 0.0.1" + # underscore-rails gem "underscore-rails", "~> 1.4.4" diff --git a/Gemfile.lock b/Gemfile.lock index 18fae9b700..5c70541a73 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -52,6 +52,7 @@ GEM sass (~> 3.2) browser (0.7.2) builder (3.2.2) + cal-heatmap-rails (0.0.1) capybara (2.2.1) mime-types (>= 1.16) nokogiri (>= 1.3.3) @@ -627,6 +628,7 @@ DEPENDENCIES binding_of_caller bootstrap-sass (~> 3.0) browser + cal-heatmap-rails (~> 0.0.1) capybara (~> 2.2.1) carrierwave coffee-rails diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index 337170605d..4912c534b0 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -39,6 +39,7 @@ #= require shortcuts_dashboard_navigation #= require shortcuts_issueable #= require shortcuts_network +#= require cal-heatmap #= require_tree . window.slugify = (text) -> diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee new file mode 100644 index 0000000000..e3bb420a27 --- /dev/null +++ b/app/assets/javascripts/calendar.js.coffee @@ -0,0 +1,71 @@ +class @calendar + options = + month: "short" + day: "numeric" + year: "numeric" + + constructor: (timestamps,starting_year,starting_month,activities_path) -> + cal = new CalHeatMap() + cal.init + itemName: ["commit"] + data: timestamps + start: new Date(starting_year, starting_month) + domainLabelFormat: "%b" + id: "cal-heatmap" + domain: "month" + subDomain: "day" + range: 12 + tooltip: true + domainDynamicDimension: false + colLimit: 4 + label: + position: "top" + domainMargin: 1 + legend: [ + 0 + 1 + 4 + 7 + ] + legendCellPadding: 3 + onClick: (date, count) -> + $.ajax + url: activities_path + data: + date: date + + dataType: "json" + success: (data) -> + $("#loading_commits").fadeIn() + calendar.calendarOnClick data, date, count + setTimeout (-> + $("#calendar_onclick_placeholder").fadeIn 500 + return + ), 400 + setTimeout (-> + $("#loading_commits").hide() + return + ), 400 + return + return + return + + @calendarOnClick: (data, date, nb)-> + $("#calendar_onclick_placeholder").hide() + $("#calendar_onclick_placeholder").html -> + "" + + ((if nb is null then "no" else nb)) + + " commit" + + ((if (nb isnt 1) then "s" else "")) + " " + + date.toLocaleDateString("en-US", options) + + "