diff --git a/.vagrant_enabled b/.vagrant_enabled new file mode 100644 index 00000000000..e69de29bb2d diff --git a/CHANGELOG b/CHANGELOG index 2467a45c0eb..ec026b8f398 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ v 8.9.0 (unreleased) - Fix issue todo not remove when leave project !4150 (Long Nguyen) - Allow forking projects with restricted visibility level - Improve note validation to prevent errors when creating invalid note via API + - Reduce number of fog gem dependencies - Remove project notification settings associated with deleted projects - Fix 404 page when viewing TODOs that contain milestones or labels in different projects - Redesign navigation for project pages @@ -27,15 +28,30 @@ v 8.9.0 (unreleased) - Measure queue duration between gitlab-workhorse and Rails - Make authentication service for Container Registry to be compatible with < Docker 1.11 - Add Application Setting to configure Container Registry token expire delay (default 5min) + - Cache assigned issue and merge request counts in sidebar nav + - Cache project build count in sidebar nav + - Reduce number of queries needed to render issue labels in the sidebar + - Improve error handling importing projects + - Put project Files and Commits tabs under Code tab v 8.8.3 - - Fix incorrect links on pipeline page when merge request created from fork - - Fix gitlab importer failing to import new projects due to missing credentials - - Fix serious performance bug with rendering Markdown with InlineDiffFilter - - Fix import URL migration not rescuing with the correct Error - - In search results, only show notes on confidential issues that the user has access to - - Fix health check access token changing due to old application settings being used - - Fix wiki project clone address error (chujinjin) + - Fix 404 page when viewing TODOs that contain milestones or labels in different projects. !4312 + - Fixed JS error when trying to remove discussion form. !4303 + - Fixed issue with button color when no CI enabled. !4287 + - Fixed potential issue with 2 CI status polling events happening. !3869 + - Improve design of Pipeline view. !4230 + - Fix gitlab importer failing to import new projects due to missing credentials. !4301 + - Fix import URL migration not rescuing with the correct Error. !4321 + - Fix health check access token changing due to old application settings being used. !4332 + - Make authentication service for Container Registry to be compatible with Docker versions before 1.11. !4363 + - Add Application Setting to configure Container Registry token expire delay (default 5 min). !4364 + - Pass the "Remember me" value to the 2FA token form. !4369 + - Fix incorrect links on pipeline page when merge request created from fork. !4376 + - Use downcased path to container repository as this is expected path by Docker. !4420 + - Fix wiki project clone address error (chujinjin). !4429 + - Fix serious performance bug with rendering Markdown with InlineDiffFilter. !4392 + - Fix missing number on generated ordered list element. !4437 + - Prevent disclosure of notes on confidential issues in search results. v 8.8.2 - Added remove due date button. !4209 diff --git a/Gemfile b/Gemfile index 0ab78c2a738..d9429de786f 100644 --- a/Gemfile +++ b/Gemfile @@ -83,8 +83,14 @@ gem "carrierwave", '~> 0.10.0' # Drag and Drop UI gem 'dropzonejs-rails', '~> 0.7.1' +# for backups +gem 'fog-aws', '~> 0.9' +gem 'fog-core', '~> 1.40' +gem 'fog-local', '~> 0.3' +gem 'fog-google', '~> 0.3' +gem 'fog-openstack', '~> 0.1' + # for aws storage -gem "fog", "~> 1.36.0" gem "unf", '~> 0.1.4' # Authorization diff --git a/Gemfile.lock b/Gemfile.lock index 4e000fa5b5b..8ae25269e65 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,6 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (2.3.2) RedCloth (4.2.9) ace-rails-ap (4.0.2) actionmailer (4.2.6) @@ -183,7 +182,7 @@ GEM erubis (2.7.0) escape_utils (1.1.1) eventmachine (1.0.8) - excon (0.45.4) + excon (0.49.0) execjs (2.6.0) expression_parser (0.9.0) factory_girl (4.5.0) @@ -200,8 +199,6 @@ GEM multi_json ffaker (2.0.0) ffi (1.9.10) - fission (0.5.0) - CFPropertyList (~> 2.2) flay (2.6.1) ruby_parser (~> 3.0) sexp_processor (~> 4.0) @@ -211,109 +208,28 @@ GEM flowdock (0.7.1) httparty (~> 0.7) multi_json - fog (1.36.0) - fog-aliyun (>= 0.1.0) - fog-atmos - fog-aws (>= 0.6.0) - fog-brightbox (~> 0.4) - fog-core (~> 1.32) - fog-dynect (~> 0.0.2) - fog-ecloud (~> 0.1) - fog-google (<= 0.1.0) - fog-json - fog-local - fog-powerdns (>= 0.1.1) - fog-profitbricks - fog-radosgw (>= 0.0.2) - fog-riakcs - fog-sakuracloud (>= 0.0.4) - fog-serverlove - fog-softlayer - fog-storm_on_demand - fog-terremark - fog-vmfusion - fog-voxel - fog-xenserver - fog-xml (~> 0.1.1) - ipaddress (~> 0.5) - nokogiri (~> 1.5, >= 1.5.11) - fog-aliyun (0.1.0) - fog-core (~> 1.27) - fog-json (~> 1.0) - ipaddress (~> 0.8) - xml-simple (~> 1.1) - fog-atmos (0.1.0) - fog-core - fog-xml - fog-aws (0.8.1) + fog-aws (0.9.2) fog-core (~> 1.27) fog-json (~> 1.0) fog-xml (~> 0.1) ipaddress (~> 0.8) - fog-brightbox (0.10.1) - fog-core (~> 1.22) - fog-json - inflecto (~> 0.0.2) - fog-core (1.35.0) + fog-core (1.40.0) builder - excon (~> 0.45) + excon (~> 0.49) formatador (~> 0.2) - fog-dynect (0.0.2) - fog-core - fog-json - fog-xml - fog-ecloud (0.3.0) - fog-core - fog-xml - fog-google (0.1.0) + fog-google (0.3.2) fog-core fog-json fog-xml fog-json (1.0.2) fog-core (~> 1.0) multi_json (~> 1.10) - fog-local (0.2.1) + fog-local (0.3.0) fog-core (~> 1.27) - fog-powerdns (0.1.1) - fog-core (~> 1.27) - fog-json (~> 1.0) - fog-xml (~> 0.1) - fog-profitbricks (0.0.5) - fog-core - fog-xml - nokogiri - fog-radosgw (0.0.5) - fog-core (>= 1.21.0) - fog-json - fog-xml (>= 0.0.1) - fog-riakcs (0.1.0) - fog-core - fog-json - fog-xml - fog-sakuracloud (1.7.5) - fog-core - fog-json - fog-serverlove (0.1.2) - fog-core - fog-json - fog-softlayer (1.0.3) - fog-core - fog-json - fog-storm_on_demand (0.1.1) - fog-core - fog-json - fog-terremark (0.1.0) - fog-core - fog-xml - fog-vmfusion (0.1.0) - fission - fog-core - fog-voxel (0.1.0) - fog-core - fog-xml - fog-xenserver (0.2.2) - fog-core - fog-xml + fog-openstack (0.1.6) + fog-core (>= 1.39) + fog-json (>= 1.0) + ipaddress (>= 0.8) fog-xml (0.1.2) fog-core nokogiri (~> 1.5, >= 1.5.11) @@ -422,11 +338,10 @@ GEM httpclient (2.7.0.1) i18n (0.7.0) ice_nine (0.11.1) - inflecto (0.0.2) influxdb (0.2.3) cause json - ipaddress (0.8.2) + ipaddress (0.8.3) jquery-atwho-rails (1.3.2) jquery-rails (4.1.1) rails-dom-testing (>= 1, < 3) @@ -873,7 +788,6 @@ GEM builder expression_parser rinku - xml-simple (1.1.5) xpath (2.0.0) nokogiri (~> 1.3) @@ -927,7 +841,11 @@ DEPENDENCIES ffaker (~> 2.0.0) flay flog - fog (~> 1.36.0) + fog-aws (~> 0.9) + fog-core (~> 1.40) + fog-google (~> 0.3) + fog-local (~> 0.3) + fog-openstack (~> 0.1) font-awesome-rails (~> 4.2) foreman fuubar (~> 2.0.0) diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee index 6a7b4ad1db7..2122e80f57a 100644 --- a/app/assets/javascripts/search_autocomplete.js.coffee +++ b/app/assets/javascripts/search_autocomplete.js.coffee @@ -20,8 +20,7 @@ class @SearchAutocomplete @dropdown = @wrap.find('.dropdown') @dropdownContent = @dropdown.find('.dropdown-content') - @locationBadgeEl = @getElement('.search-location-badge') - @locationText = @getElement('.location-text') + @locationBadgeEl = @getElement('.location-badge') @scopeInputEl = @getElement('#scope') @searchInput = @getElement('.search-input') @projectInputEl = @getElement('#search_project_id') @@ -133,7 +132,7 @@ class @SearchAutocomplete scope: @scopeInputEl.val() # Location badge - _location: @locationText.text() + _location: @locationBadgeEl.text() } bindEvents: -> @@ -143,12 +142,14 @@ class @SearchAutocomplete @searchInput.on 'click', @onSearchInputClick @searchInput.on 'focus', @onSearchInputFocus @clearInput.on 'click', @onClearInputClick + @locationBadgeEl.on 'click', => + @searchInput.focus() onDocumentClick: (e) => # If clicking outside the search box # And search input is not focused # And we are not clicking inside a suggestion - if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).parents('ul').length + if not $.contains(@dropdown[0], e.target) and @isFocused and not $(e.target).closest('.search-form').length @onSearchInputBlur() enableAutocomplete: -> @@ -221,10 +222,8 @@ class @SearchAutocomplete category = if item.category? then "#{item.category}: " else '' value = if item.value? then item.value else '' - html = " - #{category}#{value} - " - @locationBadgeEl.html(html) + badgeText = "#{category}#{value}" + @locationBadgeEl.text(badgeText).show() @wrap.addClass('has-location-badge') restoreOriginalState: -> @@ -233,9 +232,8 @@ class @SearchAutocomplete for input in inputs @getElement("##{input}").val(@originalState[input]) - if @originalState._location is '' - @locationBadgeEl.empty() + @locationBadgeEl.hide() else @addLocationBadge( value: @originalState._location @@ -244,7 +242,7 @@ class @SearchAutocomplete @dropdown.removeClass 'open' badgePresent: -> - @locationBadgeEl.children().length + @locationBadgeEl.length resetSearchState: -> inputs = Object.keys @originalState @@ -257,7 +255,7 @@ class @SearchAutocomplete @getElement("##{input}").val('') removeLocationBadge: -> - @locationBadgeEl.empty() + @locationBadgeEl.hide() # Reset state @resetSearchState() diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 5e61e61d85c..1b389d83525 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -29,8 +29,6 @@ margin-top: 6px; p { - overflow-x: auto; - &:last-child { margin-bottom: 0; } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 2bff70c8c64..037ad520545 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -28,6 +28,7 @@ } .search-input { + padding-right: 20px; border: none; font-size: 14px; outline: none; @@ -47,6 +48,7 @@ display: inline-block; background-color: $location-badge-bg; vertical-align: top; + cursor: default; } .search-input-container { @@ -55,7 +57,7 @@ position: relative; } - .search-location-badge, .search-input-wrap { + .search-input-wrap { // Fallback if flexbox is not supported display: inline-block; } diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index d09e7375b67..dd9508da049 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -50,7 +50,7 @@ class Projects::BranchesController < Projects::ApplicationController redirect_to namespace_project_branches_path(@project.namespace, @project), status: 303 end - format.js { render status: status[:return_code] } + format.js { render nothing: true, status: status[:return_code] } end end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index b9d7edb4185..b4923fbb138 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -17,7 +17,9 @@ module TodosHelper def todo_target_link(todo) target = todo.target_type.titleize.downcase - link_to "#{target} #{todo.target_reference}", todo_target_path(todo), { title: todo.target.title } + link_to "#{target} #{todo.target_reference}", todo_target_path(todo), + class: 'has-tooltip', + title: todo.target.title end def todo_target_path(todo) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 77837e9384a..b8ada6361ac 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -313,6 +313,7 @@ module Ci build_data = Gitlab::BuildDataBuilder.build(self) project.execute_hooks(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks) + project.running_or_pending_build_count(force: true) end def artifacts? diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 2326a395cb8..e86d5236abb 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -68,6 +68,14 @@ module Issuable strip_attributes :title acts_as_paranoid + + after_save :update_assignee_cache_counts, if: :assignee_id_changed? + + def update_assignee_cache_counts + # make sure we flush the cache for both the old *and* new assignee + User.find(assignee_id_was).update_cache_counts if assignee_id_was + assignee.update_cache_counts if assignee + end end module ClassMethods @@ -205,6 +213,10 @@ module Issuable hook_data end + def labels_array + labels.to_a + end + def label_names labels.order('title ASC').pluck(:title) end diff --git a/app/models/project.rb b/app/models/project.rb index e0ea1026b9c..f47ef8a81de 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1011,4 +1011,22 @@ class Project < ActiveRecord::Base update_attribute(:pending_delete, true) end + + def running_or_pending_build_count(force: false) + Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do + builds.running_or_pending.count(:all) + end + end + + def mark_import_as_failed(error_message) + original_errors = errors.dup + sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message) + + import_fail + update_column(:import_error, sanitized_message) + rescue ActiveRecord::ActiveRecordError => e + Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}") + ensure + @errors = original_errors + end end diff --git a/app/models/user.rb b/app/models/user.rb index 15b6cbc2255..172845c9d25 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -776,6 +776,23 @@ class User < ActiveRecord::Base notification_settings.find_or_initialize_by(source: source) end + def assigned_open_merge_request_count(force: false) + Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do + assigned_merge_requests.opened.count + end + end + + def assigned_open_issues_count(force: false) + Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do + assigned_issues.opened.count + end + end + + def update_cache_counts + assigned_open_merge_request_count(force: true) + assigned_open_issues_count(force: true) + end + private def projects_union diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 6728fabea1e..61cac5419ad 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -56,14 +56,14 @@ module Projects after_create_actions if @project.persisted? - @project.add_import_job if @project.import? - + if @project.errors.empty? + @project.add_import_job if @project.import? + else + fail(error: @project.errors.full_messages.join(', ')) + end @project rescue => e - message = "Unable to save project: #{e.message}" - Rails.logger.error(message) - @project.errors.add(:base, message) if @project - @project + fail(error: e.message) end protected @@ -103,5 +103,19 @@ module Projects end end end + + def fail(error:) + message = "Unable to save project. Error: #{error}" + message << "Project ID: #{@project.id}" if @project && @project.id + + Rails.logger.error(message) + + if @project && @project.import? + @project.errors.add(:base, message) + @project.mark_import_as_failed(message) + end + + @project + end end end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index ef15ef6a473..c4838d31f2f 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -39,7 +39,7 @@ module Projects begin gitlab_shell.import_repository(project.path_with_namespace, project.import_url) rescue Gitlab::Shell::Error => e - raise Error, e.message + raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}" end end diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 6b208c3d0bb..b49207fc315 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -6,11 +6,8 @@ .search.search-form{class: "#{'has-location-badge' if label.present?}"} = form_tag search_path, method: :get, class: 'navbar-form' do |f| .search-input-container - .search-location-badge - - if label.present? - %span.location-badge - %i.location-text - = label + - if label.present? + .location-badge= label .search-input-wrap .dropdown{ data: {url: search_autocomplete_path } } = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' } diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 43532b0c155..306ebd5fcf7 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -30,13 +30,13 @@ = icon('exclamation-circle fw') %span Issues - %span.count= number_with_delimiter(current_user.assigned_issues.opened.count) + %span.count= number_with_delimiter(current_user.assigned_open_issues_count) = nav_link(path: 'dashboard#merge_requests') do = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do = icon('tasks fw') %span Merge Requests - %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count) + %span.count= number_with_delimiter(current_user.assigned_open_merge_request_count) = nav_link(controller: :snippets) do = link_to dashboard_snippets_path, title: 'Snippets' do = icon('clipboard fw') diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 2c9b9006668..9792c1c93b4 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -33,18 +33,11 @@ %span Activity - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do - = icon('files-o fw') + = icon('code fw') %span - Files - - - if project_nav_tab? :commits - = nav_link(controller: %w(commit commits compare repositories tags branches releases network)) do - = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do - = icon('history fw') - %span - Commits + Code - if project_nav_tab? :pipelines = nav_link(controller: :pipelines) do @@ -129,4 +122,10 @@ = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do Builds + -# Shortcut to commits page + - if project_nav_tab? :commits + %li.hidden + = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do + Commits + .fade-right diff --git a/app/views/projects/branches/destroy.js.haml b/app/views/projects/branches/destroy.js.haml deleted file mode 100644 index a21ddaf4930..00000000000 --- a/app/views/projects/branches/destroy.js.haml +++ /dev/null @@ -1 +0,0 @@ -$('.js-totalbranch-count').html("#{@repository.branch_count}") diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index d1bd76ab529..1c136133ab0 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,9 +1,11 @@ %ul.nav-links + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_files_path(@project) do + Files + = nav_link(controller: [:commit, :commits]) do = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do Commits - %span.badge - = number_with_delimiter(@repository.commit_count) = nav_link(controller: %w(network)) do = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do @@ -16,9 +18,7 @@ = nav_link(html_options: {class: branches_tab_class}) do = link_to namespace_project_branches_path(@project.namespace, @project) do Branches - %span.badge.js-totalbranch-count= @repository.branch_count = nav_link(controller: [:tags, :releases]) do = link_to namespace_project_tags_path(@project.namespace, @project) do Tags - %span.badge.js-totaltags-count= @repository.tag_count diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index 2c8ae625e67..6e757df5417 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -11,4 +11,4 @@ = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do %span Builds - %span.badge.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) + %span.badge.count.builds_counter= number_with_delimiter(@project.running_or_pending_build_count) diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml index ffeacb5a004..e4a78fadbeb 100644 --- a/app/views/projects/tags/destroy.js.haml +++ b/app/views/projects/tags/destroy.js.haml @@ -1,3 +1,2 @@ -$('.js-totaltags-count').html("#{@repository.tags.size}"); - if @repository.tags.empty? $('.tags').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000) diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 7e9ba09c720..59f60c4687c 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -3,6 +3,7 @@ - if current_user = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") = render 'projects/last_push' += render "projects/commits/head" .tree-controls = render 'projects/find_file_link' diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index c1eec450193..d6552ae7f18 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -114,20 +114,20 @@ .sidebar-collapsed-icon = icon('tags') %span - = issuable.labels.count + = issuable.labels_array.size .title.hide-collapsed Labels = icon('spinner spin', class: 'block-loading') - if can_edit_issuable = link_to 'Edit', '#', class: 'edit-link pull-right' - .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) } - - if issuable.labels.any? - - issuable.labels.each do |label| + .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) } + - if issuable.labels_array.any? + - issuable.labels_array.each do |label| = link_to_label(label, type: issuable.to_ability_name) - else .light None .selectbox.hide-collapsed - - issuable.labels.each do |label| + - issuable.labels_array.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}} diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index f9e32337983..d947f105516 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -15,8 +15,7 @@ class RepositoryForkWorker result = gitlab_shell.fork_repository(source_path, target_path) unless result logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}") - project.update(import_error: "The project could not be forked.") - project.import_fail + project.mark_import_as_failed('The project could not be forked.') return end @@ -24,8 +23,7 @@ class RepositoryForkWorker unless project.valid_repo? logger.error("Project #{project_id} had an invalid repository after fork") - project.update(import_error: "The forked repository is invalid.") - project.import_fail + project.mark_import_as_failed('The forked repository is invalid.') return end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index fbc7ed63c6a..7d819fe78f8 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -13,8 +13,7 @@ class RepositoryImportWorker result = Projects::ImportService.new(project, current_user).execute if result[:status] == :error - project.update(import_error: Gitlab::UrlSanitizer.sanitize(result[:message])) - project.import_fail + project.mark_import_as_failed(result[:message]) return end diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md index b4dcb748351..23760a14b39 100644 --- a/doc/development/ui_guide.md +++ b/doc/development/ui_guide.md @@ -33,4 +33,24 @@ be under 'Wiki' tab and so on and so forth. We want GitLab to work well on small mobile screens as well. Size limitations make it is impossible to fit everything on a mobile screen. In this case it is OK to hide part of the UI for smaller resolutions in favor of a better user experience. However core functionality like browsing files, creating issues, writing comments, should -be available on all resolutions. \ No newline at end of file +be available on all resolutions. + +## Icons + +* `trash` icon for button or link that does destructive action like removing +information from database or file system +* `x` icon for closing/hiding UI element. For example close modal window +* `pencil` icon for edit button or link +* `eye` icon for subscribe action +* `rss` for rss/atom feed +* `plus` for link or dropdown that lead to page where you create new object (For example new issue page) + + +## Buttons + +* Button should contain icon or text. Exceptions should be approved by UX designer. +* Use gray button on white background or white button on gray background. +* Use red button for destructive actions (not revertable). For example removing issue. +* Use green or blue button for primary action. Primary button should be only one. +Do not use both green and blue button in one form. + diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index 5125a3e5773..26e67503021 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -10,14 +10,9 @@ Feature: Project Active Tab Then the active main tab should be Home And no other main tabs should be active - Scenario: On Project Files + Scenario: On Project Code Given I visit my project's files page - Then the active main tab should be Files - And no other main tabs should be active - - Scenario: On Project Commits - Given I visit my project's commits page - Then the active main tab should be Commits + Then the active main tab should be Code And no other main tabs should be active Scenario: On Project Issues @@ -64,40 +59,46 @@ Feature: Project Active Tab And no other sub navs should be active And the active main tab should be Settings - # Sub Tabs: Commits + # Sub Tabs: Code - Scenario: On Project Commits/Commits + Scenario: On Project Code/Files + Given I visit my project's files page + Then the active sub tab should be Files + And no other sub tabs should be active + And the active main tab should be Code + + Scenario: On Project Code/Commits Given I visit my project's commits page Then the active sub tab should be Commits And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Network + Scenario: On Project Code/Network Given I visit my project's network page Then the active sub tab should be Network And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Compare + Scenario: On Project Code/Compare Given I visit my project's commits page And I click the "Compare" tab Then the active sub tab should be Compare And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Branches + Scenario: On Project Code/Branches Given I visit my project's commits page And I click the "Branches" tab Then the active sub tab should be Branches And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code - Scenario: On Project Commits/Tags + Scenario: On Project Code/Tags Given I visit my project's commits page And I click the "Tags" tab Then the active sub tab should be Tags And no other sub tabs should be active - And the active main tab should be Commits + And the active main tab should be Code Scenario: On Project Issues/Browse Given I visit my project's issues page diff --git a/features/project/builds/summary.feature b/features/project/builds/summary.feature index 3c029a973df..550ebccf0d7 100644 --- a/features/project/builds/summary.feature +++ b/features/project/builds/summary.feature @@ -24,3 +24,4 @@ Feature: Project Builds Summary Then recent build has been erased And recent build summary does not have artifacts widget And recent build summary contains information saying that build has been erased + And the build count cache is updated diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature index 10e7c234610..c73d0b32337 100644 --- a/features/project/shortcuts.feature +++ b/features/project/shortcuts.feature @@ -8,19 +8,21 @@ Feature: Project Shortcuts @javascript Scenario: Navigate to files tab Given I press "g" and "f" - Then the active main tab should be Files + Then the active main tab should be Code + Then the active sub tab should be Files @javascript Scenario: Navigate to commits tab Given I visit my project's files page Given I press "g" and "c" - Then the active main tab should be Commits + Then the active main tab should be Code + Then the active sub tab should be Commits @javascript Scenario: Navigate to network tab Given I press "g" and "n" Then the active sub tab should be Network - And the active main tab should be Commits + And the active main tab should be Code @javascript Scenario: Navigate to graphs tab diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 4a5a71e7e61..745fd3471c4 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -63,10 +63,6 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps click_link('Tags') end - step 'the active sub tab should be Commits' do - ensure_active_sub_tab('Commits') - end - step 'the active sub tab should be Compare' do ensure_active_sub_tab('Compare') end diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb index e9e2359146e..374eb0b0e07 100644 --- a/features/steps/project/builds/summary.rb +++ b/features/steps/project/builds/summary.rb @@ -36,4 +36,8 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps expect(page).to have_content 'Build has been erased' end end + + step 'the build count cache is updated' do + expect(@build.project.running_or_pending_build_count).to eq @build.project.builds.running_or_pending.count(:all) + end end diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb index 8c1d09d6cc6..47de4b91df1 100644 --- a/features/steps/project/project_find_file.rb +++ b/features/steps/project/project_find_file.rb @@ -13,12 +13,12 @@ class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps end step 'I should see "find file" page' do - ensure_active_main_tab('Files') + ensure_active_main_tab('Code') expect(page).to have_selector('.file-finder-holder', count: 1) end step 'I fill in Find by path with "git"' do - ensure_active_main_tab('Files') + ensure_active_main_tab('Code') expect(page).to have_selector('.file-finder-holder', count: 1) end diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb index b209020c5a9..bfee8793301 100644 --- a/features/steps/shared/project_tab.rb +++ b/features/steps/shared/project_tab.rb @@ -8,12 +8,8 @@ module SharedProjectTab ensure_active_main_tab('Project') end - step 'the active main tab should be Files' do - ensure_active_main_tab('Files') - end - - step 'the active main tab should be Commits' do - ensure_active_main_tab('Commits') + step 'the active main tab should be Code' do + ensure_active_main_tab('Code') end step 'the active main tab should be Graphs' do @@ -51,4 +47,12 @@ module SharedProjectTab step 'the active sub tab should be Network' do ensure_active_sub_tab('Network') end + + step 'the active sub tab should be Files' do + ensure_active_sub_tab('Files') + end + + step 'the active sub tab should be Commits' do + ensure_active_sub_tab('Commits') + end end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 8ad73472117..c4b4a888b4e 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -122,27 +122,23 @@ describe Projects::BranchesController do let(:branch) { "feature" } it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } end context "valid branch name with unencoded slashes" do let(:branch) { "improve/awesome" } it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } end context "valid branch name with encoded slashes" do let(:branch) { "improve%2Fawesome" } it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } end context "invalid branch name, valid ref" do let(:branch) { "no-branch" } it { expect(response.status).to eq(404) } - it { expect(subject).to render_template('destroy') } end end end diff --git a/spec/features/project/shortcuts_spec.rb b/spec/features/projects/shortcuts_spec.rb similarity index 100% rename from spec/features/project/shortcuts_spec.rb rename to spec/features/projects/shortcuts_spec.rb diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index fb20578d8d3..e9f827e9f50 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -227,6 +227,20 @@ describe Issue, "Issuable" do end end + describe '#labels_array' do + let(:project) { create(:project) } + let(:bug) { create(:label, project: project, title: 'bug') } + let(:issue) { create(:issue, project: project) } + + before(:each) do + issue.labels << bug + end + + it 'loads the association and returns it as an array' do + expect(issue.reload.labels_array).to eq([bug]) + end + end + describe "votes" do let(:project) { issue.project } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 87b3d8d650a..b87d68283e6 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -269,4 +269,21 @@ describe Issue, models: true do end end end + + describe 'cached counts' do + it 'updates when assignees change' do + user1 = create(:user) + user2 = create(:user) + issue = create(:issue, assignee: user1) + + expect(user1.assigned_open_issues_count).to eq(1) + expect(user2.assigned_open_issues_count).to eq(0) + + issue.assignee = user2 + issue.save + + expect(user1.assigned_open_issues_count).to eq(0) + expect(user2.assigned_open_issues_count).to eq(1) + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 23d09331e5a..348f2e2f493 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -438,4 +438,21 @@ describe MergeRequest, models: true do expect(mr.participants).to include(note1.author, note2.author) end end + + describe 'cached counts' do + it 'updates when assignees change' do + user1 = create(:user) + user2 = create(:user) + mr = create(:merge_request, assignee: user1) + + expect(user1.assigned_open_merge_request_count).to eq(1) + expect(user2.assigned_open_merge_request_count).to eq(0) + + mr.assignee = user2 + mr.save + + expect(user1.assigned_open_merge_request_count).to eq(0) + expect(user2.assigned_open_merge_request_count).to eq(1) + end + end end diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 7f2dcdab960..9d90bfceb73 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -49,7 +49,7 @@ describe Projects::ImportService, services: true do result = subject.execute expect(result[:status]).to eq :error - expect(result[:message]).to eq 'Failed to import the repository' + expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Failed to import the repository" end end