diff --git a/CHANGELOG b/CHANGELOG index 6f019c1a4a..ea6d6c0ae9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,10 +5,7 @@ v 7.10.0 (unreleased) - Set Application controller default URL options to ensure all url_for calls are consistent (Stan Hu) - Allow HTML tags in Markdown input - Fix code unfold not working on Compare commits page (Stan Hu) - - Include missing events and fix save functionality in admin service template settings form (Stan Hu) - - Fix "Import projects from" button to show the correct instructions (Stan Hu) - Fix dots in Wiki slugs causing errors (Stan Hu) - - Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu) - Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg) - Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu) - Disable reference creation for comments surrounded by code/preformatted blocks (Stan Hu) @@ -17,6 +14,7 @@ v 7.10.0 (unreleased) - extend the commit calendar to show the actual commits made on a date (Hannes Rosenögger) - Fix a link in the patch update guide - Add a service to support external wikis (Hannes Rosenögger) + - Omit the "email patches" link and fix plain diff view for merge commits - List new commits for newly pushed branch in activity view. - Add sidetiq gem dependency to match EE - Add changelog, license and contribution guide links to project sidebar. @@ -24,6 +22,7 @@ v 7.10.0 (unreleased) - Fix alignment of navbar toggle button (Cody Mize) - Fix checkbox rendering for nested task lists - Identical look of selectboxes in UI + - Upgrade the gitlab_git gem to version 7.1.3 - Move "Import existing repository by URL" option to button. - Improve error message when save profile has error. - Passing the name of pushed ref to CI service (requires GitLab CI 7.9+) @@ -40,7 +39,6 @@ v 7.10.0 (unreleased) v 7.9.0 (unreleased) - Replace commits calendar with faster contribution calendar that includes issues and merge requests - Add inifinite scroll to user page activity - - Don't show commit comment button when user is not signed in. - Don't include system notes in issue/MR comment count. - Don't mark merge request as updated when merge status relative to target branch changes. - Link note avatar to user. @@ -50,13 +48,31 @@ v 7.9.0 (unreleased) - AJAX selectbox for issue assignee and author filters - Fix issue with missing options in issue filtering dropdown if selected one - Prevent holding Control-Enter or Command-Enter from posting comment multiple times. + - Prevent note form from being cleared when submitting failed. - Improve file icons rendering on tree (Sullivan Sénéchal) + - API: Add pagination to project events + - Get issue links in notification mail to work again. + - Don't show commit comment button when user is not signed in. + - Fix admin user projects lists. + - Don't leak private group existence by redirecting from namespace controller to group controller. + - Ability to skip some items from backup (database, respositories or uploads) + - Fix "Hello @username." references not working by no longer allowing usernames to end in period. + - Archive repositories in background worker. + - Import GitHub, Bitbucket or GitLab.com projects owned by authenticated user into current namespace. + - Fix and improve help rendering (Sullivan Sénéchal) -v 7.9.0 - - Send EmailsOnPush email when branch or tag is created or deleted. - - Faster merge request processing for large repository - - Prevent doubling AJAX request with each commit visit via Turbolink - - Prevent unnecessary doubling of js events on import pages and user calendar + +v 7.9.2 + - Contains no changes + +v 7.9.1 + - Include missing events and fix save functionality in admin service template settings form (Stan Hu) + - Fix "Import projects from" button to show the correct instructions (Stan Hu) + - Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu) + - Fix for LDAP with commas in DN + - Fix missing events and in admin Slack service template settings form (Stan Hu) + - Don't show commit comment button when user is not signed in. + - Downgrade gemnasium-gitlab-service gem v 7.9.0 - Add HipChat integration documentation (Stan Hu) @@ -138,6 +154,10 @@ v 7.9.0 - Fix invalid Atom feeds when using emoji, horizontal rules, or images (Christian Walther) - Backup of repositories with tar instead of git bundle (only now are git-annex files included in the backup) - Add canceled status for CI + - Send EmailsOnPush email when branch or tag is created or deleted. + - Faster merge request processing for large repository + - Prevent doubling AJAX request with each commit visit via Turbolink + - Prevent unnecessary doubling of js events on import pages and user calendar v 7.8.4 - Fix issue_tracker_id substitution in custom issue trackers diff --git a/Gemfile b/Gemfile index 011e43f31c..13f2e4a671 100644 --- a/Gemfile +++ b/Gemfile @@ -39,7 +39,7 @@ gem "browser" # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.1.2' +gem "gitlab_git", '~> 7.1.3' # Ruby/Rack Git Smart-HTTP Server Handler gem 'gitlab-grack', '~> 2.0.0.rc2', require: 'grack' @@ -116,13 +116,14 @@ end gem "state_machine" # Issue tags -gem "acts-as-taggable-on" +gem 'acts-as-taggable-on', '~> 3.4' # Background jobs gem 'slim' gem 'sinatra', require: nil gem 'sidekiq', '~> 3.3' gem 'sidetiq', '0.6.3' +gem 'sidekiq-limit_fetch' # HTTP requests gem "httparty" diff --git a/Gemfile.lock b/Gemfile.lock index f13291f118..5a3d0bdf10 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,8 +33,8 @@ GEM minitest (~> 5.1) thread_safe (~> 0.1) tzinfo (~> 1.1) - acts-as-taggable-on (2.4.1) - rails (>= 3, < 5) + acts-as-taggable-on (3.5.0) + activerecord (>= 3.2, < 5) addressable (2.3.5) annotate (2.6.0) activerecord (>= 2.3.0) @@ -212,7 +212,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.1.0) gemojione (~> 2.0) - gitlab_git (7.1.2) + gitlab_git (7.1.3) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -547,6 +547,8 @@ GEM json redis (>= 3.0.6) redis-namespace (>= 1.3.1) + sidekiq-limit_fetch (2.4.1) + sidekiq (>= 2.6.5, < 4.0) sidetiq (0.6.3) celluloid (>= 0.14.1) ice_cube (= 0.11.1) @@ -662,7 +664,7 @@ PLATFORMS DEPENDENCIES RedCloth ace-rails-ap - acts-as-taggable-on + acts-as-taggable-on (~> 3.4) addressable annotate (~> 2.6.0.beta2) asana (~> 0.0.6) @@ -703,7 +705,7 @@ DEPENDENCIES gitlab-grack (~> 2.0.0.rc2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (~> 7.1.2) + gitlab_git (~> 7.1.3) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.1) gollum-lib (~> 4.0.2) @@ -770,6 +772,7 @@ DEPENDENCIES settingslogic shoulda-matchers (~> 2.7.0) sidekiq (~> 3.3) + sidekiq-limit_fetch sidetiq (= 0.6.3) simplecov sinatra diff --git a/LICENSE b/LICENSE index 9ce9ff1497..533c8e89c3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,5 @@ The GitLab Enterprise Edition (EE) license - -Copyright (c) 2013-2014 GitLab B.V. +Copyright (c) 2011-2015 GitLab B.V. This software and associated documentation files (the "Software") can only be used with a valid GitLab subscription for the correct number of users. You are diff --git a/Procfile b/Procfile index a0ab4a734a..799b92729f 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,2 @@ web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"} -worker: bundle exec sidekiq -q post_receive -q mailer -q system_hook -q project_web_hook -q gitlab_shell -q common -q default +worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q common -q default diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee index 37b7ba2cc1..44d75bd694 100644 --- a/app/assets/javascripts/calendar.js.coffee +++ b/app/assets/javascripts/calendar.js.coffee @@ -20,9 +20,9 @@ class @Calendar position: "top" legend: [ 0 - 1 - 4 - 7 + 10 + 20 + 30 ] legendCellPadding: 3 onClick: (date, count) -> diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 204e10eb32..012acf2a19 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -104,6 +104,8 @@ class Dispatcher new GroupsSelect() when 'admin:emails:show' new AdminEmailSelect() + when 'admin:users:show' + new ProjectsList() switch path.first() when 'admin' diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index bf71c144ea..0aaa62d2b8 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -9,12 +9,8 @@ class @Issue if $("a.btn-close").length $("li.task-list-item input:checkbox").prop("disabled", false) - $(".task-list-item input:checkbox").on( - "click" - null - "issue" - updateTaskState - ) + $('.task-list-item input:checkbox').off('change') + $('.task-list-item input:checkbox').change('issue', updateTaskState) $('.issue-details').waitForImages -> $('.issuable-affix').affix offset: diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee index 6127d2bb48..2cbe03b986 100644 --- a/app/assets/javascripts/merge_request.js.coffee +++ b/app/assets/javascripts/merge_request.js.coffee @@ -81,12 +81,8 @@ class @MergeRequest this.$('.remove_source_branch_in_progress').hide() this.$('.remove_source_branch_widget.failed').show() - $(".task-list-item input:checkbox").on( - "click" - null - "merge_request" - updateTaskState - ) + $('.task-list-item input:checkbox').off('change') + $('.task-list-item input:checkbox').change('merge_request', updateTaskState) activateTab: (action) -> this.$('.merge-request-tabs li').removeClass 'active' diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index b61c4dd654..6dfe10f000 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -37,7 +37,8 @@ class @Notes $(document).on "click", ".js-note-attachment-delete", @removeAttachment # reset main target form after submit - $(document).on "ajax:complete", ".js-main-target-form", @resetMainTargetForm + $(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton + $(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm # update the file name when an attachment is selected $(document).on "change", ".js-note-attachment-input", @updateFormAttachment @@ -71,6 +72,7 @@ class @Notes $(document).off "click", ".js-note-delete" $(document).off "click", ".js-note-attachment-delete" $(document).off "ajax:complete", ".js-main-target-form" + $(document).off "ajax:success", ".js-main-target-form" $(document).off "click", ".js-discussion-reply-button" $(document).off "click", ".js-add-diff-note-button" $(document).off "visibilitychange" @@ -170,6 +172,11 @@ class @Notes form.find(".js-note-text").data("autosave").reset() + reenableTargetFormSubmitButton: -> + form = $(".js-main-target-form") + + form.find(".js-note-text").trigger "input" + ### Shows the main form and does some setup on it. diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee index 7febcba0e9..2e3f560825 100644 --- a/app/assets/javascripts/sidebar.js.coffee +++ b/app/assets/javascripts/sidebar.js.coffee @@ -3,12 +3,7 @@ $(document).on("click", '.toggle-nav-collapse', (e) -> collapsed = 'page-sidebar-collapsed' expanded = 'page-sidebar-expanded' - if $('.page-with-sidebar').hasClass(collapsed) - $('.page-with-sidebar').removeClass(collapsed).addClass(expanded) - $('.toggle-nav-collapse i').removeClass('fa-angle-right').addClass('fa-angle-left') - $.cookie("collapsed_nav", "false", { path: '/' }) - else - $('.page-with-sidebar').removeClass(expanded).addClass(collapsed) - $('.toggle-nav-collapse i').removeClass('fa-angle-left').addClass('fa-angle-right') - $.cookie("collapsed_nav", "true", { path: '/' }) + $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") + $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left") + $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) ) diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 9f035381e6..d187bc7327 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -246,7 +246,7 @@ li.note { .milestone { &.milestone-closed { - background: #eee; + background: #f9f9f9; } .progress { margin-bottom: 0; diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/generic/mobile.scss index 1b0e056216..71a1fc4493 100644 --- a/app/assets/stylesheets/generic/mobile.scss +++ b/app/assets/stylesheets/generic/mobile.scss @@ -24,13 +24,6 @@ display: none !important; } - .project-home-panel { - .star-fork-buttons { - padding-top: 10px; - padding-right: 15px; - } - } - .project-home-links { display: none; } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 6d55a5fa66..5bd725d122 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -49,35 +49,20 @@ @extend .clearfix; margin-bottom: 15px; - .project-home-desc, - .star-fork-buttons { + &.project-home-row-top { + margin-bottom: 15px; + } + + .project-home-desc { font-size: 16px; line-height: 1.3; + margin-right: 215px; } .project-home-desc { float: left; color: #666; } - - .star-fork-buttons { - float: right; - min-width: 200px; - font-weight: bold; - - .star-buttons, .fork-buttons { - float: right; - margin-left: 20px; - - a:hover { - text-decoration: none; - } - - .count { - margin-left: 5px; - } - } - } } .visibility-level-label { @@ -87,6 +72,27 @@ color: inherit; } } + + .project-repo-buttons { + margin-top: -3px; + position: absolute; + right: 0; + width: 260px; + text-align: right; + + .btn { + font-weight: bold; + font-size: 14px; + line-height: 16px; + + .count { + padding-left: 10px; + border-left: 1px solid #ccc; + display: inline-block; + margin-left: 10px; + } + } + } } .project-home-links { diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 968823ac81..fbd9e67e6d 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -3,23 +3,37 @@ class HelpController < ApplicationController end def show - @category = params[:category] - @file = params[:file] - format = params[:format] || 'md' - file_path = Rails.root.join('doc', @category, @file + ".#{format}") + @filepath = params[:filepath] + @format = params[:format] - if %w(png jpg jpeg gif).include?(format) - send_file file_path, disposition: 'inline' - elsif File.exists?(file_path) - render 'show' - else - not_found! + respond_to do |format| + format.md { render_doc } + format.all { send_file_data } end end def shortcuts end + private + + def render_doc + if File.exists?(Rails.root.join('doc', @filepath + '.md')) + render 'show.html.haml' + else + not_found! + end + end + + def send_file_data + path = Rails.root.join('doc', "#{@filepath}.#{@format}") + if File.exists?(path) + send_file(path, disposition: 'inline') + else + head :not_found + end + end + def ui end end diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 83ebc5fddc..bb8d7e0235 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -36,8 +36,11 @@ class Import::BitbucketController < Import::BaseController def create @repo_id = params[:repo_id] || "" repo = client.project(@repo_id.gsub("___", "/")) - @target_namespace = params[:new_namespace].presence || repo["owner"] @project_name = repo["slug"] + + repo_owner = repo["owner"] + repo_owner = current_user.username if repo_owner == client.user["user"]["username"] + @target_namespace = params[:new_namespace].presence || repo_owner namespace = get_or_create_namespace || (render and return) diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 8650b6464d..87b41454c7 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -31,8 +31,11 @@ class Import::GithubController < Import::BaseController def create @repo_id = params[:repo_id].to_i repo = client.repo(@repo_id) - @target_namespace = params[:new_namespace].presence || repo.owner.login @project_name = repo.name + + repo_owner = repo.owner.login + repo_owner = current_user.username if repo_owner == client.user.login + @target_namespace = params[:new_namespace].presence || repo_owner namespace = get_or_create_namespace || (render and return) diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index e979dad4b1..bddbfded81 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -28,8 +28,11 @@ class Import::GitlabController < Import::BaseController def create @repo_id = params[:repo_id].to_i repo = client.project(@repo_id) - @target_namespace = params[:new_namespace].presence || repo["namespace"]["path"] @project_name = repo["name"] + + repo_owner = repo["namespace"]["path"] + repo_owner = current_user.username if repo_owner == client.user["username"] + @target_namespace = params[:new_namespace].presence || repo_owner namespace = get_or_create_namespace || (render and return) diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb index b7a9d8c129..386d103ee5 100644 --- a/app/controllers/namespaces_controller.rb +++ b/app/controllers/namespaces_controller.rb @@ -4,14 +4,22 @@ class NamespacesController < ApplicationController def show namespace = Namespace.find_by(path: params[:id]) - unless namespace - return render_404 + if namespace + if namespace.is_a?(Group) + group = namespace + else + user = namespace.owner + end end - if namespace.type == "Group" - redirect_to group_path(namespace) + if user + redirect_to user_path(user) + elsif group && can?(current_user, :read_group, group) + redirect_to group_path(group) + elsif current_user.nil? + authenticate_user! else - redirect_to user_path(namespace.owner) + render_404 end end end diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index cbb888b25e..96defb0c72 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -11,18 +11,18 @@ class Projects::RepositoriesController < Projects::ApplicationController end def archive - unless can?(current_user, :download_code, @project) - render_404 and return + begin + file_path = ArchiveRepositoryService.new(@project, params[:ref], params[:format]).execute + rescue + return head :not_found end - file_path = ArchiveRepositoryService.new.execute(@project, params[:ref], params[:format]) - if file_path # Send file to user response.headers["Content-Length"] = File.open(file_path).size.to_s send_file file_path else - render_404 + redirect_to request.fullpath end end end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 3386fac865..9703c8d9e9 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -29,6 +29,10 @@ module GitlabRoutingHelper namespace_project_merge_request_path(entity.project.namespace, entity.project, entity, *args) end + def milestone_path(entity, *args) + namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args) + end + def project_url(project, *args) namespace_project_url(project.namespace, project, *args) end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 187e21832f..a9030729b4 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -40,7 +40,7 @@ module IconsHelper def file_type_icon_class(type, mode, name) if type == 'folder' icon_class = 'folder' - elsif mode == 0120000 + elsif mode == '120000' icon_class = 'share' else # Guess which icon to choose based on file extension. diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index a4bd4d3021..ad4a761272 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -13,22 +13,34 @@ module IssuesHelper OpenStruct.new(id: 0, title: 'None (backlog)', name: 'Unassigned') end - def url_for_project_issues(project = @project) + def url_for_project_issues(project = @project, options = {}) return '' if project.nil? - project.issues_tracker.project_url + if options[:only_path] + project.issues_tracker.project_path + else + project.issues_tracker.project_url + end end - def url_for_new_issue(project = @project) + def url_for_new_issue(project = @project, options = {}) return '' if project.nil? - project.issues_tracker.new_issue_url + if options[:only_path] + project.issues_tracker.new_issue_path + else + project.issues_tracker.new_issue_url + end end - def url_for_issue(issue_iid, project = @project) + def url_for_issue(issue_iid, project = @project, options = {}) return '' if project.nil? - project.issues_tracker.issue_url(issue_iid) + if options[:only_path] + project.issues_tracker.issue_path(issue_iid) + else + project.issues_tracker.issue_url(issue_iid) + end end def title_for_issue(issue_iid, project = @project) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index e464dbd2ed..103a8187c7 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -80,17 +80,17 @@ module ProjectsHelper @project.milestones.active.order("due_date, title ASC") end - def link_to_toggle_star(title, starred, signed_in) - cls = 'star-btn' - cls << ' disabled' unless signed_in + def link_to_toggle_star(title, starred) + cls = 'star-btn btn btn-sm btn-default' + + toggle_text = + if starred + ' Unstar' + else + ' Star' + end toggle_html = content_tag('span', class: 'toggle') do - toggle_text = if starred - ' Unstar' - else - ' Star' - end - icon('star') + toggle_text end @@ -106,23 +106,25 @@ module ProjectsHelper data: { type: 'json' } } + path = toggle_star_namespace_project_path(@project.namespace, @project) content_tag 'span', class: starred ? 'turn-on' : 'turn-off' do - link_to( - toggle_star_namespace_project_path(@project.namespace, @project), - link_opts - ) do + link_to(path, link_opts) do toggle_html + ' ' + count_html end end end def link_to_toggle_fork - out = icon('code-fork') - out << ' Fork' - out << content_tag(:span, class: 'count') do + html = content_tag('span') do + icon('code-fork') + ' Fork' + end + + count_html = content_tag(:span, class: 'count') do @project.forks_count.to_s end + + html + count_html end private diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index c9eeec731f..9dcce3c991 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -52,7 +52,7 @@ module Mentionable if identifier == "all" users.push(*project.team.members.flatten) elsif namespace = Namespace.find_by(path: identifier) - if namespace.type == "Group" + if namespace.is_a?(Group) users.push(*namespace.users) else users << namespace.owner diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 35280889a8..dd74165f88 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -24,8 +24,8 @@ class Namespace < ActiveRecord::Base validates :name, presence: true, uniqueness: true, length: { within: 0..255 }, - format: { with: Gitlab::Regex.name_regex, - message: Gitlab::Regex.name_regex_message } + format: { with: Gitlab::Regex.namespace_name_regex, + message: Gitlab::Regex.namespace_name_regex_message } validates :description, length: { within: 0..255 } validates :path, @@ -33,8 +33,8 @@ class Namespace < ActiveRecord::Base presence: true, length: { within: 1..255 }, exclusion: { in: Gitlab::Blacklist.path }, - format: { with: Gitlab::Regex.path_regex, - message: Gitlab::Regex.path_regex_message } + format: { with: Gitlab::Regex.namespace_regex, + message: Gitlab::Regex.namespace_regex_message } delegate :name, to: :owner, allow_nil: true, prefix: true @@ -44,21 +44,36 @@ class Namespace < ActiveRecord::Base scope :root, -> { where('type IS NULL') } - def self.by_path(path) - where('lower(path) = :value', value: path.downcase).first - end + class << self + def by_path(path) + where('lower(path) = :value', value: path.downcase).first + end - # Case insensetive search for namespace by path or name - def self.find_by_path_or_name(path) - find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase) - end + # Case insensetive search for namespace by path or name + def find_by_path_or_name(path) + find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase) + end - def self.search(query) - where("name LIKE :query OR path LIKE :query", query: "%#{query}%") - end + def search(query) + where("name LIKE :query OR path LIKE :query", query: "%#{query}%") + end - def self.global_id - 'GLN' + def clean_path(path) + path.gsub!(/@.*\z/, "") + path.gsub!(/\.git\z/, "") + path.gsub!(/\A-/, "") + path.gsub!(/.\z/, "") + path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + + counter = 0 + base = path + while Namespace.by_path(path).present? + counter += 1 + path = "#{base}#{counter}" + end + + path + end end def to_param diff --git a/app/models/project.rb b/app/models/project.rb index 1d997d15f9..650a9392a7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -130,12 +130,12 @@ class Project < ActiveRecord::Base presence: true, length: { within: 0..255 }, format: { with: Gitlab::Regex.project_name_regex, - message: Gitlab::Regex.project_regex_message } + message: Gitlab::Regex.project_name_regex_message } validates :path, presence: true, length: { within: 0..255 }, - format: { with: Gitlab::Regex.path_regex, - message: Gitlab::Regex.path_regex_message } + format: { with: Gitlab::Regex.project_path_regex, + message: Gitlab::Regex.project_path_regex_message } validates :issues_enabled, :merge_requests_enabled, :wiki_enabled, inclusion: { in: [true, false] } validates :issues_tracker_id, length: { maximum: 255 }, allow_blank: true diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 84346350a6..5f0553f3b0 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -20,8 +20,13 @@ class GitlabIssueTrackerService < IssueTrackerService include Rails.application.routes.url_helpers - prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url + default_url_options[:host] = Gitlab.config.gitlab.host + default_url_options[:protocol] = Gitlab.config.gitlab.protocol + default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port? + default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root + + prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url def default? true @@ -32,20 +37,26 @@ class GitlabIssueTrackerService < IssueTrackerService end def project_url - "#{gitlab_url}#{namespace_project_issues_path(project.namespace, project)}" + namespace_project_issues_url(project.namespace, project) end def new_issue_url - "#{gitlab_url}#{new_namespace_project_issue_path(namespace_id: project.namespace, project_id: project)}" + new_namespace_project_issue_url(namespace_id: project.namespace, project_id: project) end def issue_url(iid) - "#{gitlab_url}#{namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: iid)}" + namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: iid) end - private + def project_path + namespace_project_issues_path(project.namespace, project) + end - def gitlab_url - Gitlab.config.gitlab.relative_url_root.chomp("/") if Gitlab.config.gitlab.relative_url_root + def new_issue_path + new_namespace_project_issue_path(namespace_id: project.namespace, project_id: project) + end + + def issue_path(iid) + namespace_project_issue_path(namespace_id: project.namespace, 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 7afd16d2d7..7840d4204f 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -38,6 +38,18 @@ class IssueTrackerService < Service self.issues_url.gsub(':id', iid.to_s) end + def project_path + project_url + end + + def new_issue_path + new_issue_url + end + + def issue_path(iid) + issue_url(iid) + end + def fields [ { type: 'text', name: 'description', placeholder: description }, diff --git a/app/models/repository.rb b/app/models/repository.rb index 77765cae1a..7276949887 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -267,6 +267,9 @@ class Repository # Remove archives older than 2 hours def clean_old_archives repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path + + return unless File.directory?(repository_downloads_path) + Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete)) end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 3fb2ec1d66..b35e72c4bd 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -33,8 +33,8 @@ class Snippet < ActiveRecord::Base validates :file_name, presence: true, length: { within: 0..255 }, - format: { with: Gitlab::Regex.path_regex, - message: Gitlab::Regex.path_regex_message } + format: { with: Gitlab::Regex.file_name_regex, + message: Gitlab::Regex.file_name_regex_message } validates :content, presence: true validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values } diff --git a/app/models/user.rb b/app/models/user.rb index 87652f2d03..1985017c24 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -129,8 +129,8 @@ class User < ActiveRecord::Base presence: true, uniqueness: { case_sensitive: false }, exclusion: { in: Gitlab::Blacklist.path }, - format: { with: Gitlab::Regex.username_regex, - message: Gitlab::Regex.username_regex_message } + format: { with: Gitlab::Regex.namespace_regex, + message: Gitlab::Regex.namespace_regex_message } validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true validate :namespace_uniq, if: ->(user) { user.username_changed? } @@ -238,7 +238,7 @@ class User < ActiveRecord::Base def non_ldap joins('LEFT JOIN identities ON identities.user_id = users.id'). - where('identities.provider IS NULL OR identities.provider NOT LIKE ?', 'ldap%') + where('identities.provider IS NULL OR identities.provider NOT LIKE ?', 'ldap%') end def clean_username(username) diff --git a/app/services/archive_repository_service.rb b/app/services/archive_repository_service.rb index 8823f6fdc6..e1b41527d8 100644 --- a/app/services/archive_repository_service.rb +++ b/app/services/archive_repository_service.rb @@ -1,14 +1,62 @@ class ArchiveRepositoryService - def execute(project, ref, format) - storage_path = Gitlab.config.gitlab.repository_downloads_path + attr_reader :project, :ref, :format - unless File.directory?(storage_path) - FileUtils.mkdir_p(storage_path) + def initialize(project, ref, format) + format ||= 'tar.gz' + @project, @ref, @format = project, ref, format.downcase + end + + def execute(options = {}) + project.repository.clean_old_archives + + raise "No archive file path" unless file_path + + return file_path if archived? + + unless archiving? + RepositoryArchiveWorker.perform_async(project.id, ref, format) end - format ||= 'tar.gz' - repository = project.repository - repository.clean_old_archives - repository.archive_repo(ref, storage_path, format.downcase) + archived = wait_until_archived(options[:timeout] || 5.0) + + file_path if archived + end + + private + + def storage_path + Gitlab.config.gitlab.repository_downloads_path + end + + def file_path + @file_path ||= project.repository.archive_file_path(ref, storage_path, format) + end + + def pid_file_path + @pid_file_path ||= project.repository.archive_pid_file_path(ref, storage_path, format) + end + + def archived? + File.exist?(file_path) + end + + def archiving? + File.exist?(pid_file_path) + end + + def wait_until_archived(timeout = 5.0) + return archived? if timeout == 0.0 + + t1 = Time.now + + begin + sleep 0.1 + + success = archived? + + t2 = Time.now + end until success || t2 - t1 >= timeout + + success end end diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 66f55608c5..300e500706 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -16,10 +16,10 @@ module Files file_name = File.basename(path) file_path = path - unless file_name =~ Gitlab::Regex.path_regex + unless file_name =~ Gitlab::Regex.file_name_regex return error( 'Your changes could not be committed, because the file name ' + - Gitlab::Regex.path_regex_message + Gitlab::Regex.file_name_regex_message ) end diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml new file mode 100644 index 0000000000..21e730bb7f --- /dev/null +++ b/app/views/dashboard/milestones/_milestone.html.haml @@ -0,0 +1,20 @@ +%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } + %h4 + = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title) + .row + .col-sm-6 + = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do + = pluralize milestone.issue_count, 'Issue' +   + = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do + = pluralize milestone.merge_requests_count, 'Merge Request' +   + %span.light #{milestone.percent_complete}% complete + + .col-sm-6 + = milestone_progress_bar(milestone) + %div + - milestone.milestones.each do |milestone| + = link_to milestone_path(milestone) do + %span.label.label-gray + = milestone.project.name_with_namespace diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index caf3b68586..9944c0df81 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -16,23 +16,5 @@ .nothing-here-block No milestones to show - else - @dashboard_milestones.each do |milestone| - %li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } - %h4 - = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title) - %div - %div - = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do - = pluralize milestone.issue_count, 'Issue' -   - = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do - = pluralize milestone.merge_requests_count, 'Merge Request' -   - %span.light #{milestone.percent_complete}% complete - = milestone_progress_bar(milestone) - %div - %br - - milestone.milestones.each do |milestone| - = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) do - %span.label.label-default - = milestone.project.name_with_namespace + = render 'milestone', milestone: milestone = paginate @dashboard_milestones, theme: "gitlab" diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml index 13e4e2fae1..fb7b5a85fb 100644 --- a/app/views/groups/group_members/_new_group_member.html.haml +++ b/app/views/groups/group_members/_new_group_member.html.haml @@ -1,7 +1,7 @@ = form_for @group_member, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f| .form-group = f.label :user_ids, "People", class: 'control-label' - .col-sm-10= users_select_tag(:user_ids, { multiple: true, skip_ldap: @group.ldap_synced? , class: 'input-large' }) + .col-sm-10= users_select_tag(:user_ids, multiple: true, skip_ldap: @group.ldap_synced?, class: 'input-large', scope: :all) .form-group = f.label :access_level, "Group Access", class: 'control-label' diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml new file mode 100644 index 0000000000..94fc43a581 --- /dev/null +++ b/app/views/groups/milestones/_milestone.html.haml @@ -0,0 +1,25 @@ +%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } + .pull-right + - if can?(current_user, :manage_group, @group) + - if milestone.closed? + = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" + - else + = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close" + %h4 + = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title) + .row + .col-sm-6 + = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do + = pluralize milestone.issue_count, 'Issue' +   + = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do + = pluralize milestone.merge_requests_count, 'Merge Request' +   + %span.light #{milestone.percent_complete}% complete + .col-sm-6 + = milestone_progress_bar(milestone) + %div + - milestone.milestones.each do |milestone| + = link_to milestone_path(milestone) do + %span.label.label-gray + = milestone.project.name diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index 57dc235f5b..008d5a6bd2 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -18,29 +18,5 @@ .nothing-here-block No milestones to show - else - @group_milestones.each do |milestone| - %li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) } - .pull-right - - if can?(current_user, :manage_group, @group) - - if milestone.closed? - = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen" - - else - = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close" - %h4 - = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title) - %div - %div - = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do - = pluralize milestone.issue_count, 'Issue' -   - = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do - = pluralize milestone.merge_requests_count, 'Merge Request' -   - %span.light #{milestone.percent_complete}% complete - = milestone_progress_bar(milestone) - %div - %br - - milestone.milestones.each do |milestone| - = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) do - %span.label.label-default - = milestone.project.name + = render 'milestone', milestone: milestone = paginate @group_milestones, theme: "gitlab" diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml index eca34dbff0..f22aa92caf 100644 --- a/app/views/help/show.html.haml +++ b/app/views/help/show.html.haml @@ -1,2 +1,2 @@ .documentation.wiki - = markdown File.read(Rails.root.join('doc', @category, @file + '.md')).gsub("$your_email", current_user.email) + = markdown File.read(Rails.root.join('doc', @filepath + '.md')).gsub("$your_email", current_user.email) diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 52681865d6..6c13f30f62 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -53,7 +53,7 @@ - if project_nav_tab? :issues = nav_link(controller: :issues) do - = link_to url_for_project_issues, title: 'Issues', class: 'shortcuts-issues' do + = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do %i.fa.fa-exclamation-circle %span Issues diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml index 778a78acf5..e796353cec 100644 --- a/app/views/notify/_note_message.html.haml +++ b/app/views/notify/_note_message.html.haml @@ -1,2 +1,2 @@ %div - = replace_image_links_with_base64(markdown(@note.note), @note.project) + = replace_image_links_with_base64(markdown(@note.note, reference_only_path: false), @note.project) diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml index 03cbee9460..d4d413b5b4 100644 --- a/app/views/notify/new_issue_email.html.haml +++ b/app/views/notify/new_issue_email.html.haml @@ -1,5 +1,5 @@ -if @issue.description - = replace_image_links_with_base64(markdown(@issue.description), @issue.project) + = replace_image_links_with_base64(markdown(@issue.description, reference_only_path: false), @issue.project) - if @issue.assignee_id.present? %p diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index 729a7bb505..60e33227e0 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -6,4 +6,4 @@ Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name} -if @merge_request.description - = replace_image_links_with_base64(markdown(@merge_request.description), @merge_request.project) + = replace_image_links_with_base64(markdown(@merge_request.description, reference_only_path: false), @merge_request.project) diff --git a/app/views/projects/_dropdown.html.haml b/app/views/projects/_dropdown.html.haml index f4f4c2662c..3adb308728 100644 --- a/app/views/projects/_dropdown.html.haml +++ b/app/views/projects/_dropdown.html.haml @@ -5,7 +5,7 @@ %ul.dropdown-menu - if @project.issues_enabled && can?(current_user, :write_issue, @project) %li - = link_to url_for_new_issue, title: "New Issue" do + = link_to url_for_new_issue(@project, only_path: true), title: "New Issue" do New issue - if @project.merge_requests_enabled && can?(current_user, :write_merge_request, @project) %li diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index a295a0d6cd..5689bdee1c 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -2,7 +2,7 @@ .project-home-panel{:class => ("empty-project" if empty_repo)} .project-identicon-holder = project_icon(@project, alt: '', class: 'avatar project-avatar') - .project-home-row + .project-home-row.project-home-row-top .project-home-desc - if @project.description.present? = escaped_autolink(@project.description) @@ -14,31 +14,30 @@ – = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do = readme.name - .star-fork-buttons + .project-repo-buttons + .inline.star.js-toggler-container{class: @show_star ? 'on' : ''} + - if current_user + = link_to_toggle_star('Star this project.', false) + = link_to_toggle_star('Unstar this project.', true) + - else + = link_to new_user_session_path, class: 'btn star-btn has_tooltip', title: 'You must sign in to star a project' do + %span + = icon('star') + Star + %span.count + = @project.star_count - unless @project.empty_repo? - .fork-buttons - - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace + - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace + .inline.fork-buttons.prepend-left-10 - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork' do + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-sm btn-default' do = link_to_toggle_fork - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project" do + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-sm btn-default' do = link_to_toggle_fork - .star-buttons - %span.star.js-toggler-container{class: @show_star ? 'on' : ''} - - if current_user - = link_to_toggle_star('Star this project.', false, true) - = link_to_toggle_star('Unstar this project.', true, true) - - else - = link_to_toggle_star('You must sign in to star a project.', false, false) - .project-home-row.hidden-xs - if current_user && !empty_repo .project-home-dropdown = render "dropdown" - - unless @project.empty_repo? - - if can? current_user, :download_code, @project - .pull-right.prepend-left-10 - = render 'projects/repositories/download_archive', split_button: true = render "shared/clone_panel" diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 2579f2cac9..3f645b8139 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -10,7 +10,8 @@ Download as %span.caret %ul.dropdown-menu - %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch) + - unless @commit.parents.length > 1 + %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch) %li= link_to "Plain Diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff) = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-primary btn-grouped" do %span Browse Code » diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 83e4d24cf5..a714f5f79e 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -1,6 +1,8 @@ %ul.nav.nav-tabs = nav_link(controller: [:commit, :commits]) do - = link_to 'Commits', namespace_project_commits_path(@project.namespace, @project, @repository.root_ref) + = link_to namespace_project_commits_path(@project.namespace, @project, @repository.root_ref) do + Commits + %span.badge= number_with_precision(@repository.commit_count, precision: 0, delimiter: ',') = nav_link(controller: :compare) do = link_to 'Compare', namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref) diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml index 7039c85bb2..62360158ff 100644 --- a/app/views/projects/milestones/_milestone.html.haml +++ b/app/views/projects/milestones/_milestone.html.haml @@ -11,16 +11,14 @@ %span.cred (Expired) %small = milestone.expires_at - - if milestone.is_empty? - %span.muted Empty - - else - %div - %div - = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do - = pluralize milestone.issues.count, 'Issue' -   - = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do - = pluralize milestone.merge_requests.count, 'Merge Request' -   - %span.light #{milestone.percent_complete}% complete + .row + .col-sm-6 + = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do + = pluralize milestone.issues.count, 'Issue' +   + = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do + = pluralize milestone.merge_requests.count, 'Merge Request' +   + %span.light #{milestone.percent_complete}% complete + .col-sm-6 = milestone_progress_bar(milestone) diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml index 49ce6c0888..35c15cf3a9 100644 --- a/app/views/projects/refs/logs_tree.js.haml +++ b/app/views/projects/refs/logs_tree.js.haml @@ -15,5 +15,5 @@ if(current_url == log_url) { // Load 10 more commit log for each file in tree // if we still on the same page - ajaxGet('#{logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '/', offset: (@offset + @limit))}'); + ajaxGet('#{logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: (@offset + @limit))}'); } diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml index 26669fb00a..1ba7a1f206 100644 --- a/app/views/projects/repositories/_download_archive.html.haml +++ b/app/views/projects/repositories/_download_archive.html.haml @@ -3,10 +3,10 @@ - split_button = split_button || false - if split_button == true %span.btn-group{class: btn_class} - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn btn-sm', rel: 'nofollow' do %i.fa.fa-download %span Download zip - %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } + %a.btn-sm.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' } %span.caret %span.sr-only Select Archive Format diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index cfa6f558dd..85113ffa7e 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -68,7 +68,7 @@ = link_to license_url(@project), class: 'btn btn-block' do View license - .prepend-top-10 + .prepend-top-10.append-bottom-10 %p %span.light Created on #{@project.created_at.stamp('Aug 22, 2013')} @@ -79,8 +79,16 @@ - else #{link_to @project.owner_name, @project.owner} + - unless @project.empty_repo? + - if can? current_user, :download_code, @project + %hr + .prepend-top-10.append-bottom-10 + = render 'projects/repositories/download_archive', split_button: true + + .prepend-top-10 - @project.ci_services.each do |ci_service| - if ci_service.active? && ci_service.respond_to?(:builds_path) + %hr - if ci_service.respond_to?(:status_img_path) = link_to ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' do = image_tag ci_service.status_img_path, alt: "build status" @@ -97,4 +105,3 @@ = readme.name .wiki = render_readme(readme) - diff --git a/app/views/users/_projects.html.haml b/app/views/users/_projects.html.haml index b7383d5594..297fa53739 100644 --- a/app/views/users/_projects.html.haml +++ b/app/views/users/_projects.html.haml @@ -1,13 +1,13 @@ -- if @contributed_projects.present? +- if local_assigns.has_key?(:contributed_projects) && contributed_projects.present? .panel.panel-default.contributed-projects .panel-heading Projects contributed to = render 'shared/projects_list', - projects: @contributed_projects.sort_by(&:star_count).reverse, + projects: contributed_projects.sort_by(&:star_count).reverse, projects_limit: 5, stars: true, avatar: false -- if @projects.present? +- if local_assigns.has_key?(:projects) && projects.present? .panel.panel-default .panel-heading Personal projects = render 'shared/projects_list', - projects: @projects.sort_by(&:star_count).reverse, + projects: projects.sort_by(&:star_count).reverse, projects_limit: 10, stars: true, avatar: false diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 5e1d65e2ed..9dd8cb0738 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -44,7 +44,7 @@ = spinner %aside.col-md-4 = render 'profile', user: @user - = render 'projects' + = render 'projects', projects: @projects, contributed_projects: @contributed_projects :coffeescript $(".user-calendar").load("#{user_calendar_path}") diff --git a/app/workers/repository_archive_worker.rb b/app/workers/repository_archive_worker.rb new file mode 100644 index 0000000000..021c113956 --- /dev/null +++ b/app/workers/repository_archive_worker.rb @@ -0,0 +1,43 @@ +class RepositoryArchiveWorker + include Sidekiq::Worker + + sidekiq_options queue: :archive_repo + + attr_accessor :project, :ref, :format + + def perform(project_id, ref, format) + @project = Project.find(project_id) + @ref, @format = ref, format.downcase + + repository = project.repository + + repository.clean_old_archives + + return unless file_path + return if archived? || archiving? + + repository.archive_repo(ref, storage_path, format) + end + + private + + def storage_path + Gitlab.config.gitlab.repository_downloads_path + end + + def file_path + @file_path ||= project.repository.archive_file_path(ref, storage_path, format) + end + + def pid_file_path + @pid_file_path ||= project.repository.archive_pid_file_path(ref, storage_path, format) + end + + def archived? + File.exist?(file_path) + end + + def archiving? + File.exist?(pid_file_path) + end +end diff --git a/bin/background_jobs b/bin/background_jobs index 59a51c5c86..a041a4b043 100755 --- a/bin/background_jobs +++ b/bin/background_jobs @@ -37,7 +37,7 @@ start_no_deamonize() start_sidekiq() { - bundle exec sidekiq -q post_receive -q mailer -q system_hook -q project_web_hook -q gitlab_shell -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1 + bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1 } load_ok() diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index ad32b25f5e..a05e7d99f4 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -66,8 +66,8 @@ production: &base # If a commit message matches this regular expression, all issues referenced from the matched text will be closed. # This happens when the commit is pushed or merged into the default branch of a project. # When not specified the default issue_closing_pattern as specified below will be used. - # Tip: you can test your closing pattern at http://rubular.com - # issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?)|([A-Z]*-\d*))+)' + # Tip: you can test your closing pattern at http://rubular.com. + # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' ## Default project features settings default_projects_features: @@ -86,35 +86,6 @@ production: &base # The default is 'tmp/repositories' relative to the root of the Rails app. # repository_downloads_path: tmp/repositories - ## External issues trackers - issues_tracker: - # redmine: - # title: "Redmine" - # ## If not nil, link 'Issues' on project page will be replaced with this - # ## Use placeholders: - # ## :project_id - GitLab project identifier - # ## :issues_tracker_id - Project Name or Id in external issue tracker - # project_url: "http://redmine.sample/projects/:issues_tracker_id" - # - # ## If not nil, links from /#\d/ entities from commit messages will replaced with this - # ## Use placeholders: - # ## :project_id - GitLab project identifier - # ## :issues_tracker_id - Project Name or Id in external issue tracker - # ## :id - Issue id (from commit messages) - # issues_url: "http://redmine.sample/issues/:id" - # - # ## If not nil, links to creating new issues will be replaced with this - # ## Use placeholders: - # ## :project_id - GitLab project identifier - # ## :issues_tracker_id - Project Name or Id in external issue tracker - # new_issue_url: "http://redmine.sample/projects/:issues_tracker_id/issues/new" - # - # jira: - # title: "Atlassian Jira" - # project_url: "http://jira.sample/issues/?jql=project=:issues_tracker_id" - # issues_url: "http://jira.sample/browse/:id" - # new_issue_url: "http://jira.sample/secure/CreateIssue.jspa" - ## Gravatar ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html gravatar: diff --git a/config/initializers/4_sidekiq.rb b/config/initializers/4_sidekiq.rb index e856499732..da2e8a08b0 100644 --- a/config/initializers/4_sidekiq.rb +++ b/config/initializers/4_sidekiq.rb @@ -25,3 +25,5 @@ Sidekiq.configure_client do |config| namespace: 'resque:gitlab' } end + +Sidekiq::Queue["archive_repo"].limit = 2 diff --git a/config/initializers/acts_as_taggable_on_patch.rb b/config/initializers/acts_as_taggable_on_patch.rb deleted file mode 100644 index 0d535cb5ca..0000000000 --- a/config/initializers/acts_as_taggable_on_patch.rb +++ /dev/null @@ -1,131 +0,0 @@ -# This is a patch to address the issue in https://github.com/mbleigh/acts-as-taggable-on/issues/427 caused by -# https://github.com/rails/rails/commit/31a43ebc107fbd50e7e62567e5208a05909ec76c -# gem 'acts-as-taggable-on' has the fix included https://github.com/mbleigh/acts-as-taggable-on/commit/89bbed3864a9252276fb8dd7d535fce280454b90 -# but not in the currently used version of gem ('2.4.1') -# With replacement of 'acts-as-taggable-on' gem this file will become obsolete - -module ActsAsTaggableOn::Taggable - module Core - module ClassMethods - def tagged_with(tags, options = {}) - tag_list = ActsAsTaggableOn::TagList.from(tags) - empty_result = where("1 = 0") - - return empty_result if tag_list.empty? - - joins = [] - conditions = [] - having = [] - select_clause = [] - - context = options.delete(:on) - owned_by = options.delete(:owned_by) - alias_base_name = undecorated_table_name.gsub('.','_') - quote = ActsAsTaggableOn::Tag.using_postgresql? ? '"' : '' - - if options.delete(:exclude) - if options.delete(:wild) - tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ? ESCAPE '!'", "%#{escape_like(t)}%"]) }.join(" OR ") - else - tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ") - end - - conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)})" - - if owned_by - joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" + - " ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" + - " AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}" + - " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{owned_by.id}" + - " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s, nil)}" - end - - elsif options.delete(:any) - # get tags, drop out if nothing returned (we need at least one) - tags = - if options.delete(:wild) - ActsAsTaggableOn::Tag.named_like_any(tag_list) - else - ActsAsTaggableOn::Tag.named_any(tag_list) - end - - return empty_result unless tags.length > 0 - - # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123 - # avoid ambiguous column name - taggings_context = context ? "_#{context}" : '' - - taggings_alias = adjust_taggings_alias( - "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}" - ) - - tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" + - " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" + - " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" - tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context - - # don't need to sanitize sql, map all ids and join with OR logic - conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ") - select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one? - - if owned_by - tagging_join << " AND " + - sanitize_sql([ - "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?", - owned_by.id, - owned_by.class.base_class.to_s - ]) - end - - joins << tagging_join - else - tags = ActsAsTaggableOn::Tag.named_any(tag_list) - - return empty_result unless tags.length == tag_list.length - - tags.each do |tag| - taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}") - tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" + - " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" + - " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" + - " AND #{taggings_alias}.tag_id = #{tag.id}" - - tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context - - if owned_by - tagging_join << " AND " + - sanitize_sql([ - "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?", - owned_by.id, - owned_by.class.base_class.to_s - ]) - end - - joins << tagging_join - end - end - - taggings_alias, tags_alias = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group" - - if options.delete(:match_all) - joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" + - " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" + - " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" - - - group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}" - group = group_columns - having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}" - end - - select(select_clause) \ - .joins(joins.join(" ")) \ - .where(conditions.join(" AND ")) \ - .group(group) \ - .having(having) \ - .order(options[:order]) \ - .readonly(false) - end - end - end -end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 8f8bef42be..6978ad9302 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -6,3 +6,4 @@ Mime::Type.register_alias "text/plain", :diff Mime::Type.register_alias "text/plain", :patch +Mime::Type.register_alias 'text/html', :md diff --git a/config/routes.rb b/config/routes.rb index 54120c02b3..8567e3b8a2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -39,10 +39,9 @@ Gitlab::Application.routes.draw do # Help get 'help' => 'help#index' - get 'help/:category/:file' => 'help#show', as: :help_page - get 'help/:category/*file' => 'help#show' get 'help/shortcuts' get 'help/ui' => 'help#ui' + get 'help/:filepath' => 'help#show', as: :help_page, constraints: { filepath: /[^\.]+/ } # # Global snippets diff --git a/db/migrate/20150324133047_remove_periods_at_ends_of_usernames.rb b/db/migrate/20150324133047_remove_periods_at_ends_of_usernames.rb new file mode 100644 index 0000000000..7ce53c2a0d --- /dev/null +++ b/db/migrate/20150324133047_remove_periods_at_ends_of_usernames.rb @@ -0,0 +1,76 @@ +class RemovePeriodsAtEndsOfUsernames < ActiveRecord::Migration + include Gitlab::ShellAdapter + + class Namespace < ActiveRecord::Base + class << self + def by_path(path) + where('lower(path) = :value', value: path.downcase).first + end + + def clean_path(path) + path = path.dup + path.gsub!(/@.*\z/, "") + path.gsub!(/\.git\z/, "") + path.gsub!(/\A-/, "") + path.gsub!(/.\z/, "") + path.gsub!(/[^a-zA-Z0-9_\-\.]/, "") + + counter = 0 + base = path + while Namespace.by_path(path).present? + counter += 1 + path = "#{base}#{counter}" + end + + path + end + end + end + + def up + changed_paths = {} + + select_all("SELECT id, username FROM users WHERE username LIKE '%.'").each do |user| + username_was = user["username"] + username = Namespace.clean_path(username_was) + changed_paths[username_was] = username + + username = quote_string(username) + execute "UPDATE users SET username = '#{username}' WHERE id = #{user["id"]}" + execute "UPDATE namespaces SET path = '#{username}', name = '#{username}' WHERE type IS NULL AND owner_id = #{user["id"]}" + end + + select_all("SELECT id, path FROM namespaces WHERE type = 'Group' AND path LIKE '%.'").each do |group| + path_was = group["path"] + path = Namespace.clean_path(path_was) + changed_paths[path_was] = path + + path = quote_string(path) + execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{group["id"]}" + end + + changed_paths.each do |path_was, path| + if gitlab_shell.mv_namespace(path_was, path) + # If repositories moved successfully we need to remove old satellites + # and send update instructions to users. + # However we cannot allow rollback since we moved namespace dir + # So we basically we mute exceptions in next actions + begin + gitlab_shell.rm_satellites(path_was) + # We cannot send update instructions since models and mailers + # can't safely be used from migrations as they may be written for + # later versions of the database. + # send_update_instructions + rescue + # Returning false does not rollback after_* transaction but gives + # us information about failing some of tasks + false + end + else + # if we cannot move namespace directory we should rollback + # db changes in order to prevent out of sync between db and fs + raise Exception.new('namespace directory cannot be moved') + end + end + end +end diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md index ddc0c8eac2..aa65a082a5 100644 --- a/doc/customization/issue_closing.md +++ b/doc/customization/issue_closing.md @@ -1,5 +1,36 @@ # Issue closing pattern -By default you can close issues from commit messages by saying 'Closes #12' or 'Fixed #101'. +If a commit message matches the regular expression below, all issues referenced from +the matched text will be closed. This happens when the commit is pushed or merged +into the default branch of a project. -If you want to customize the message please do so in [gitlab.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/73b92f85bcd6c213b845cc997843a969cf0906cf/config/gitlab.yml.example#L73) +When not specified, the default issue_closing_pattern as shown below will be used: + +```bash +((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+) +``` + +For example: + +``` +git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes #22). This commit is also related to #17 and fixes #18, #19 and #23." +``` + +will close `#20`, `#21`, `#22`, `#18`, `#19` and `#23`, but `#17` won't be closed +as it does not match the pattern. It also works with multiline commit messages. + +Tip: you can test this closing pattern at [http://rubular.com][1]. Use this site +to test your own patterns. + +## Change the pattern + +For Omnibus installs you can change the default pattern in `/etc/gitlab/gitlab.rb`: + +``` +issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' +``` + +For manual installs you can customize the pattern in [gitlab.yml][0]. + +[0]: https://gitlab.com/gitlab-org/gitlab-ce/blob/40c3675372320febf5264061c9bcd63db2dfd13c/config/gitlab.yml.example#L65 +[1]: http://rubular.com/r/Xmbexed1OJ diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 99cdfff0ac..2e41fad89e 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -17,6 +17,13 @@ sudo gitlab-rake gitlab:backup:create sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` +Also you can choose what should be backed up by adding environment variable SKIP. Available options: db, +uploads (attachments), repositories. Use a comma to specify several options at the same time. + +``` +sudo gitlab-rake gitlab:backup:create SKIP=db,uploads +``` + Example output: ``` diff --git a/docker/Dockerfile b/docker/Dockerfile index b228a66832..f0a8b9f53d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,7 +11,7 @@ RUN apt-get update -q \ # If the Omnibus package version below is outdated please contribute a merge request to update it. # If you run GitLab Enterprise Edition point it to a location where you have downloaded it. RUN TMP_FILE=$(mktemp); \ - wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.9.0-omnibus.2-1_amd64.deb \ + wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.9.1-omnibus.1-1_amd64.deb \ && dpkg -i $TMP_FILE \ && rm -f $TMP_FILE diff --git a/features/groups.feature b/features/groups.feature index 963eaa3e56..4e675efffd 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -54,6 +54,14 @@ Feature: Groups Then I see a new LDAP synchronization listed And LDAP disabled + @javascript + Scenario: Add user to group + Given gitlab user "Mike" + When I visit group "Owned" members page + And I click link "Add members" + When I select "Mike" as "Reporter" + Then I should see "Mike" in team list as "Reporter" + # Leave @javascript diff --git a/features/project/star.feature b/features/project/star.feature index 3322f89180..a45f9c470e 100644 --- a/features/project/star.feature +++ b/features/project/star.feature @@ -13,7 +13,7 @@ Feature: Project Star Given public project "Community" And I visit project "Community" page When I click on the star toggle button - Then The project has 0 stars + Then I redirected to sign in page @javascript Scenario: Signed in users can toggle star diff --git a/features/steps/dashboard/help.rb b/features/steps/dashboard/help.rb index 177581b8f3..fa52e391f0 100644 --- a/features/steps/dashboard/help.rb +++ b/features/steps/dashboard/help.rb @@ -8,7 +8,7 @@ class Spinach::Features::DashboardHelp < Spinach::FeatureSteps end step 'I visit the "Rake Tasks" help page' do - visit help_page_path("raketasks", "maintenance.md") + visit help_page_path('raketasks/maintenance', format: 'md') end step 'I should see "Rake Tasks" page markdown rendered' do diff --git a/features/steps/groups.rb b/features/steps/groups.rb index fe0db5e1e0..d0b334e1ed 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -5,6 +5,32 @@ class Spinach::Features::Groups < Spinach::FeatureSteps include SharedUser include Select2Helper + step 'gitlab user "Mike"' do + create(:user, name: "Mike") + end + + step 'I click link "Add members"' do + find(:css, 'button.btn-new').click + end + + step 'I select "Mike" as "Reporter"' do + user = User.find_by(name: "Mike") + + within ".users-group-form" do + select2(user.id, from: "#user_ids", multiple: true) + select "Reporter", from: "access_level" + end + + click_button "Add users to group" + end + + step 'I should see "Mike" in team list as "Reporter"' do + within '.well-list' do + page.should have_content('Mike') + page.should have_content('Reporter') + end + end + step 'I should see group "Owned" projects list' do Group.find_by(name: "Owned").projects.each do |project| page.should have_link project.name diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 557555aee5..caf6c73ee0 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -74,7 +74,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I fill the new file name with an illegal name' do - fill_in :file_name, with: '.git' + fill_in :file_name, with: 'Spaces Not Allowed' end step 'I fill the commit message' do diff --git a/features/steps/project/star.rb b/features/steps/project/star.rb index ae2e4c7a20..50cdfd73c3 100644 --- a/features/steps/project/star.rb +++ b/features/steps/project/star.rb @@ -22,12 +22,16 @@ class Spinach::Features::ProjectStar < Spinach::FeatureSteps # Requires @javascript step "I click on the star toggle button" do - find(".star .toggle", visible: true).click + find(".star-btn", visible: true).click + end + + step 'I redirected to sign in page' do + current_path.should == new_user_session_path end protected def has_n_stars(n) - expect(page).to have_css(".star .count", text: /^#{n}$/, visible: true) + expect(page).to have_css(".star-btn .count", text: n, visible: true) end end diff --git a/lib/api/branches.rb b/lib/api/branches.rb index edfdf842f8..592100a704 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -1,5 +1,4 @@ require 'mime/types' -require 'uri' module API # Projects API @@ -101,10 +100,11 @@ module API # branch (required) - The name of the branch # Example Request: # DELETE /projects/:id/repository/branches/:branch - delete ":id/repository/branches/:branch" do + delete ":id/repository/branches/:branch", + requirements: { branch: /.*/ } do authorize_push_project result = DeleteBranchService.new(user_project, current_user). - execute(URI.unescape(params[:branch])) + execute(params[:branch]) if result[:status] == :success { diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 293dc6427d..0c6d1ff64f 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -88,17 +88,14 @@ module API present user_project, with: Entities::ProjectWithAccess, user: current_user end - # Get a single project events + # Get events for a single project # # Parameters: # id (required) - The ID of a project # Example Request: # GET /projects/:id/events get ":id/events" do - limit = (params[:per_page] || 20).to_i - offset = (params[:page] || 0).to_i * limit - events = user_project.events.recent.limit(limit).offset(offset) - + events = paginate user_project.events.recent present events, with: Entities::Event end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index b259914a01..1fbf3dca3c 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -133,10 +133,11 @@ module API authorize! :download_code, user_project begin - file_path = ArchiveRepositoryService.new.execute( - user_project, - params[:sha], - params[:format]) + file_path = ArchiveRepositoryService.new( + user_project, + params[:sha], + params[:format] + ).execute rescue not_found!('File') end @@ -149,7 +150,7 @@ module API env['api.format'] = :binary present data else - not_found!('File') + redirect request.fullpath end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index afd0589750..b69aebf9fe 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -1,7 +1,5 @@ module Backup class Manager - BACKUP_CONTENTS = %w{repositories/ db/ uploads/ backup_information.yml} - def pack # saving additional informations s = {} @@ -9,6 +7,7 @@ module Backup s[:backup_created_at] = Time.now s[:gitlab_version] = Gitlab::VERSION s[:tar_version] = tar_version + s[:skipped] = ENV["SKIP"] tar_file = "#{s[:backup_created_at].to_i}_gitlab_backup.tar" Dir.chdir(Gitlab.config.backup.path) do @@ -17,12 +16,12 @@ module Backup file << s.to_yaml.gsub(/^---\n/,'') end - FileUtils.chmod_R(0700, %w{db uploads repositories}) + FileUtils.chmod(0700, folders_to_backup) # create archive $progress.print "Creating backup archive: #{tar_file} ... " orig_umask = File.umask(0077) - if Kernel.system('tar', '-cf', tar_file, *BACKUP_CONTENTS) + if Kernel.system('tar', '-cf', tar_file, *backup_contents) $progress.puts "done".green else puts "creating archive #{tar_file} failed".red @@ -46,6 +45,7 @@ module Backup connection = ::Fog::Storage.new(connection_settings) directory = connection.directories.get(remote_directory) + if directory.files.create(key: tar_file, body: File.open(tar_file), public: false) $progress.puts "done".green else @@ -56,7 +56,10 @@ module Backup def cleanup $progress.print "Deleting tmp directories ... " - BACKUP_CONTENTS.each do |dir| + + backup_contents.each do |dir| + next unless File.exist?(File.join(Gitlab.config.backup.path, dir)) + if FileUtils.rm_rf(File.join(Gitlab.config.backup.path, dir)) $progress.puts "done".green else @@ -73,6 +76,7 @@ module Backup if keep_time > 0 removed = 0 + Dir.chdir(Gitlab.config.backup.path) do file_list = Dir.glob('*_gitlab_backup.tar') file_list.map! { |f| $1.to_i if f =~ /(\d+)_gitlab_backup.tar/ } @@ -84,6 +88,7 @@ module Backup end end end + $progress.puts "done. (#{removed} removed)".green else $progress.puts "skipping".yellow @@ -96,6 +101,7 @@ module Backup # check for existing backups in the backup dir file_list = Dir.glob("*_gitlab_backup.tar").each.map { |f| f.split(/_/).first.to_i } puts "no backups found" if file_list.count == 0 + if file_list.count > 1 && ENV["BACKUP"].nil? puts "Found more than one backup, please specify which one you want to restore:" puts "rake gitlab:backup:restore BACKUP=timestamp_of_backup" @@ -110,6 +116,7 @@ module Backup end $progress.print "Unpacking backup ... " + unless Kernel.system(*%W(tar -xf #{tar_file})) puts "unpacking backup failed".red exit 1 @@ -117,7 +124,6 @@ module Backup $progress.puts "done".green end - settings = YAML.load_file("backup_information.yml") ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0 # restoring mismatching backups can lead to unexpected problems @@ -136,5 +142,29 @@ module Backup tar_version, _ = Gitlab::Popen.popen(%W(tar --version)) tar_version.force_encoding('locale').split("\n").first end + + def skipped?(item) + settings[:skipped] && settings[:skipped].include?(item) + end + + private + + def backup_contents + folders_to_backup + ["backup_information.yml"] + end + + def folders_to_backup + folders = %w{repositories db uploads} + + if ENV["SKIP"] + return folders.reject{ |folder| ENV["SKIP"].include?(folder) } + end + + folders + end + + def settings + @settings ||= YAML.load_file("backup_information.yml") + end end end diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb index f48ede9d06..9c00896c91 100644 --- a/lib/gitlab/gitlab_import/client.rb +++ b/lib/gitlab/gitlab_import/client.rb @@ -28,6 +28,10 @@ module Gitlab client.auth_code.get_token(code, redirect_uri: redirect_uri).token end + def user + api.get("/api/v3/user").parsed + end + def issues(project_identifier) lazy_page_iterator(PER_PAGE) do |page| api.get("/api/v3/projects/#{project_identifier}/issues?per_page=#{PER_PAGE}&page=#{page}").parsed diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 41bb8d0892..f1e2ae74a3 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -32,12 +32,12 @@ module Gitlab module Markdown include IssuesHelper - attr_reader :html_options + attr_reader :options, :html_options # Public: Parse the provided text with GitLab-Flavored Markdown # # text - the source text - # project - extra options for the reference links as given to link_to + # project - the project # html_options - extra options for the reference links as given to link_to def gfm(text, project = @project, html_options = {}) gfm_with_options(text, {}, project, html_options) @@ -46,9 +46,10 @@ module Gitlab # Public: Parse the provided text with GitLab-Flavored Markdown # # text - the source text - # options - parse_tasks: true - render tasks - # - xhtml: true - output XHTML instead of HTML - # project - extra options for the reference links as given to link_to + # options - parse_tasks - render tasks + # - xhtml - output XHTML instead of HTML + # - reference_only_path - Use relative path for reference links + # project - the project # html_options - extra options for the reference links as given to link_to def gfm_with_options(text, options = {}, project = @project, html_options = {}) return text if text.nil? @@ -58,6 +59,13 @@ module Gitlab # for gsub calls to work as we need them to. text = text.dup.to_str + options.reverse_merge!( + parse_tasks: false, + xhtml: false, + reference_only_path: true + ) + + @options = options @html_options = html_options # Extract pre blocks so they are not altered @@ -113,12 +121,13 @@ module Gitlab markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline result = markdown_pipeline.call(text, markdown_context) - saveoptions = 0 + + save_options = 0 if options[:xhtml] - saveoptions |= Nokogiri::XML::Node::SaveOptions::AS_XHTML + save_options |= Nokogiri::XML::Node::SaveOptions::AS_XHTML end - text = result[:output].to_html(save_with: saveoptions) + text = result[:output].to_html(save_with: save_options) if options[:parse_tasks] text = parse_tasks(text) @@ -152,7 +161,7 @@ module Gitlab text end - NAME_STR = '[a-zA-Z0-9_][a-zA-Z0-9_\-\.]*' + NAME_STR = Gitlab::Regex::NAMESPACE_REGEX_STR PROJ_STR = "(?#{NAME_STR}/#{NAME_STR})" REFERENCE_PATTERN = %r{ @@ -229,33 +238,37 @@ module Gitlab end def reference_user(identifier, project = @project, _ = nil) - options = html_options.merge( + link_options = html_options.merge( class: "gfm gfm-project_member #{html_options[:class]}" ) if identifier == "all" - link_to("@all", namespace_project_url(project.namespace, project), options) + link_to( + "@all", + namespace_project_url(project.namespace, project, only_path: options[:reference_only_path]), + link_options + ) elsif namespace = Namespace.find_by(path: identifier) url = - if namespace.type == "Group" - group_url(identifier) - else - user_url(identifier) + if namespace.is_a?(Group) + group_url(identifier, only_path: options[:reference_only_path]) + else + user_url(identifier, only_path: options[:reference_only_path]) end - - link_to("@#{identifier}", url, options) + + link_to("@#{identifier}", url, link_options) end end def reference_label(identifier, project = @project, _ = nil) if label = project.labels.find_by(id: identifier) - options = html_options.merge( + link_options = html_options.merge( class: "gfm gfm-label #{html_options[:class]}" ) link_to( render_colored_label(label), namespace_project_issues_path(project.namespace, project, label_name: label.name), - options + link_options ) end end @@ -263,14 +276,14 @@ module Gitlab def reference_issue(identifier, project = @project, prefix_text = nil) if project.default_issues_tracker? if project.issue_exists? identifier - url = url_for_issue(identifier, project) + url = url_for_issue(identifier, project, only_path: options[:reference_only_path]) title = title_for_issue(identifier, project) - options = html_options.merge( + link_options = html_options.merge( title: "Issue: #{title}", class: "gfm gfm-issue #{html_options[:class]}" ) - link_to("#{prefix_text}##{identifier}", url, options) + link_to("#{prefix_text}##{identifier}", url, link_options) end else if project.external_issue_tracker.present? @@ -280,44 +293,46 @@ module Gitlab end end - def reference_merge_request(identifier, project = @project, - prefix_text = nil) + def reference_merge_request(identifier, project = @project, prefix_text = nil) if merge_request = project.merge_requests.find_by(iid: identifier) - options = html_options.merge( + link_options = html_options.merge( title: "Merge Request: #{merge_request.title}", class: "gfm gfm-merge_request #{html_options[:class]}" ) url = namespace_project_merge_request_url(project.namespace, project, - merge_request) - link_to("#{prefix_text}!#{identifier}", url, options) + merge_request, + only_path: options[:reference_only_path]) + link_to("#{prefix_text}!#{identifier}", url, link_options) end end def reference_snippet(identifier, project = @project, _ = nil) if snippet = project.snippets.find_by(id: identifier) - options = html_options.merge( + link_options = html_options.merge( title: "Snippet: #{snippet.title}", class: "gfm gfm-snippet #{html_options[:class]}" ) link_to( "$#{identifier}", - namespace_project_snippet_url(project.namespace, project, snippet), - options + namespace_project_snippet_url(project.namespace, project, snippet, + only_path: options[:reference_only_path]), + link_options ) end end def reference_commit(identifier, project = @project, prefix_text = nil) if project.valid_repo? && commit = project.repository.commit(identifier) - options = html_options.merge( + link_options = html_options.merge( title: commit.link_title, class: "gfm gfm-commit #{html_options[:class]}" ) prefix_text = "#{prefix_text}@" if prefix_text link_to( "#{prefix_text}#{identifier}", - namespace_project_commit_url(project.namespace, project, commit), - options + namespace_project_commit_url( project.namespace, project, commit, + only_path: options[:reference_only_path]), + link_options ) end end @@ -328,11 +343,11 @@ module Gitlab inclusive = identifier !~ /\.{3}/ from_id << "^" if inclusive - if project.valid_repo? && - from = project.repository.commit(from_id) && + if project.valid_repo? && + from = project.repository.commit(from_id) && to = project.repository.commit(to_id) - options = html_options.merge( + link_options = html_options.merge( title: "Commits #{from_id} through #{to_id}", class: "gfm gfm-commit_range #{html_options[:class]}" ) @@ -340,22 +355,23 @@ module Gitlab link_to( "#{prefix_text}#{identifier}", - namespace_project_compare_url(project.namespace, project, from: from_id, to: to_id), - options + namespace_project_compare_url(project.namespace, project, + from: from_id, to: to_id, + only_path: options[:reference_only_path]), + link_options ) end end - def reference_external_issue(identifier, project = @project, - prefix_text = nil) - url = url_for_issue(identifier, project) + def reference_external_issue(identifier, project = @project, prefix_text = nil) + url = url_for_issue(identifier, project, only_path: options[:reference_only_path]) title = project.external_issue_tracker.title - options = html_options.merge( + link_options = html_options.merge( title: "Issue in #{title}", class: "gfm gfm-issue #{html_options[:class]}" ) - link_to("#{prefix_text}##{identifier}", url, options) + link_to("#{prefix_text}##{identifier}", url, link_options) end # Turn list items that start with "[ ]" into HTML checkbox inputs. diff --git a/lib/gitlab/oauth/user.rb b/lib/gitlab/oauth/user.rb index c023d27570..2f5c217d76 100644 --- a/lib/gitlab/oauth/user.rb +++ b/lib/gitlab/oauth/user.rb @@ -86,7 +86,7 @@ module Gitlab def user_attributes { name: auth_hash.name, - username: ::User.clean_username(auth_hash.username), + username: ::Namespace.clean_path(auth_hash.username), email: auth_hash.email, password: auth_hash.password, password_confirmation: auth_hash.password, diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index cf6e260f25..0571574aa4 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -2,49 +2,66 @@ module Gitlab module Regex extend self - def username_regex - default_regex + NAMESPACE_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])'.freeze + + def namespace_regex + @namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze end - def username_regex_message - default_regex_message + def namespace_regex_message + "can contain only letters, digits, '_', '-' and '.'. " \ + "Cannot start with '-' or end in '.'." \ end + + def namespace_name_regex + @namespace_name_regex ||= /\A[a-zA-Z0-9_\-\. ]*\z/.freeze + end + + def namespace_name_regex_message + "can contain only letters, digits, '_', '-', '.' and space." + end + + def project_name_regex - /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\. ]*\z/ + @project_name_regex ||= /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\. ]*\z/.freeze end - def project_regex_message - "can contain only letters, digits, '_', '-' and '.' and space. " \ + def project_name_regex_message + "can contain only letters, digits, '_', '-', '.' and space. " \ "It must start with letter, digit or '_'." end - def name_regex - /\A[a-zA-Z0-9_\-\. ]*\z/ + + def project_path_regex + @project_path_regex ||= /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\.]*(?why not inspect !#{merge_request.iid}

\n") + expect(markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})")).to eq("

why not inspect !#{merge_request.iid}

\n") end it "should leave ref-like src of images untouched" do diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb index 0b1cf07b7b..c052981fe7 100644 --- a/spec/helpers/icons_helper_spec.rb +++ b/spec/helpers/icons_helper_spec.rb @@ -7,7 +7,7 @@ describe IconsHelper do end it 'returns share class' do - expect(file_type_icon_class('file', 0120000, 'link')).to eq 'share' + expect(file_type_icon_class('file', '120000', 'link')).to eq 'share' end it 'returns file-pdf-o class with .pdf' do diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 1db9f15b79..727884c41c 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -1,14 +1,14 @@ require 'spec_helper' describe Gitlab::Regex do - describe 'path regex' do - it { expect('gitlab-ce').to match(Gitlab::Regex.path_regex) } - it { expect('gitlab_git').to match(Gitlab::Regex.path_regex) } - it { expect('_underscore.js').to match(Gitlab::Regex.path_regex) } - it { expect('100px.com').to match(Gitlab::Regex.path_regex) } - it { expect('?gitlab').not_to match(Gitlab::Regex.path_regex) } - it { expect('git lab').not_to match(Gitlab::Regex.path_regex) } - it { expect('gitlab.git').not_to match(Gitlab::Regex.path_regex) } + describe 'project path regex' do + it { expect('gitlab-ce').to match(Gitlab::Regex.project_path_regex) } + it { expect('gitlab_git').to match(Gitlab::Regex.project_path_regex) } + it { expect('_underscore.js').to match(Gitlab::Regex.project_path_regex) } + it { expect('100px.com').to match(Gitlab::Regex.project_path_regex) } + it { expect('?gitlab').not_to match(Gitlab::Regex.project_path_regex) } + it { expect('git lab').not_to match(Gitlab::Regex.project_path_regex) } + it { expect('gitlab.git').not_to match(Gitlab::Regex.project_path_regex) } end describe 'project name regex' do diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index ed6845c82c..e87432fdf6 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -33,8 +33,6 @@ describe Namespace do it { is_expected.to respond_to(:to_param) } end - it { expect(Namespace.global_id).to eq('GLN') } - describe :to_param do it { expect(namespace.to_param).to eq(namespace.path) } end @@ -85,4 +83,14 @@ describe Namespace do it { expect(Namespace.find_by_path_or_name('WOW')).to eq(@namespace) } it { expect(Namespace.find_by_path_or_name('unknown')).to eq(nil) } end + + describe ".clean_path" do + + let!(:user) { create(:user, username: "johngitlab-etc") } + let!(:namespace) { create(:namespace, path: "JohnGitLab-etc1") } + + it "cleans the path and makes sure it's available" do + expect(Namespace.clean_path("-john+gitlab-ETC%.git@gmail.com")).to eq("johngitlab-ETC2") + end + end end diff --git a/spec/models/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb similarity index 100% rename from spec/models/asana_service_spec.rb rename to spec/models/project_services/asana_service_spec.rb diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb index 959044dc72..f94bef5c36 100644 --- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb +++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb @@ -31,6 +31,7 @@ describe GitlabIssueTrackerService do context 'with absolute urls' do before do + GitlabIssueTrackerService.default_url_options[:script_name] = "/gitlab/root" @service = project.create_gitlab_issue_tracker_service(active: true) end @@ -39,15 +40,15 @@ describe GitlabIssueTrackerService do end it 'should give the correct path' do - expect(@service.project_url).to eq("/#{project.path_with_namespace}/issues") - expect(@service.new_issue_url).to eq("/#{project.path_with_namespace}/issues/new") - expect(@service.issue_url(432)).to eq("/#{project.path_with_namespace}/issues/432") + expect(@service.project_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues") + expect(@service.new_issue_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/new") + expect(@service.issue_url(432)).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/432") end end - context 'with enabled relative urls' do + context 'with relative urls' do before do - Settings.gitlab.stub(:relative_url_root).and_return("/gitlab/root") + GitlabIssueTrackerService.default_url_options[:script_name] = "/gitlab/root" @service = project.create_gitlab_issue_tracker_service(active: true) end @@ -56,9 +57,9 @@ describe GitlabIssueTrackerService do end it 'should give the correct path' do - expect(@service.project_url).to eq("/gitlab/root/#{project.path_with_namespace}/issues") - expect(@service.new_issue_url).to eq("/gitlab/root/#{project.path_with_namespace}/issues/new") - expect(@service.issue_url(432)).to eq("/gitlab/root/#{project.path_with_namespace}/issues/432") + expect(@service.project_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues") + expect(@service.new_issue_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues/new") + expect(@service.issue_path(432)).to eq("/gitlab/root/#{project.path_with_namespace}/issues/432") end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ac20e0e817..2ee185a69c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -320,16 +320,6 @@ describe User do end end - describe ".clean_username" do - - let!(:user) { create(:user, username: "johngitlab-etc") } - let!(:namespace) { create(:namespace, path: "JohnGitLab-etc1") } - - it "cleans a username and makes sure it's available" do - expect(User.clean_username("-john+gitlab-ETC%.git@gmail.com")).to eq("johngitlab-ETC2") - end - end - describe 'all_ssh_keys' do it { is_expected.to have_many(:keys).dependent(:destroy) } diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 9fb76b751c..e99ee93eaa 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -247,12 +247,12 @@ describe API::API, api: true do expect(json_response['message']['name']).to eq([ 'can\'t be blank', 'is too short (minimum is 0 characters)', - Gitlab::Regex.project_regex_message + Gitlab::Regex.project_name_regex_message ]) expect(json_response['message']['path']).to eq([ 'can\'t be blank', 'is too short (minimum is 0 characters)', - Gitlab::Regex.send(:default_regex_message) + Gitlab::Regex.send(:project_path_regex_message) ]) end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 71427f712b..8fb590a58e 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -152,7 +152,7 @@ describe API::API, api: true do expect(json_response['message']['projects_limit']). to eq(['must be greater than or equal to 0']) expect(json_response['message']['username']). - to eq([Gitlab::Regex.send(:default_regex_message)]) + to eq([Gitlab::Regex.send(:namespace_regex_message)]) end it "shouldn't available for non admin users" do @@ -278,7 +278,7 @@ describe API::API, api: true do expect(json_response['message']['projects_limit']). to eq(['must be greater than or equal to 0']) expect(json_response['message']['username']). - to eq([Gitlab::Regex.send(:default_regex_message)]) + to eq([Gitlab::Regex.send(:namespace_regex_message)]) end context "with existing user" do diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index d4915b5195..f5db548f97 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -73,41 +73,41 @@ end # help_markdown GET /help/markdown(.:format) help#markdown # help_ssh GET /help/ssh(.:format) help#ssh # help_raketasks GET /help/raketasks(.:format) help#raketasks -describe HelpController, "routing" do - it "to #index" do - expect(get("/help")).to route_to('help#index') +describe HelpController, 'routing' do + it 'to #index' do + expect(get('/help')).to route_to('help#index') end - it "to #permissions" do - expect(get("/help/permissions/permissions")).to route_to('help#show', category: "permissions", file: "permissions") + it 'to #permissions' do + expect(get('/help/permissions/permissions')).to route_to('help#show', filepath: 'permissions/permissions') end - it "to #workflow" do - expect(get("/help/workflow/README")).to route_to('help#show', category: "workflow", file: "README") + it 'to #workflow' do + expect(get('/help/workflow/README')).to route_to('help#show', filepath: 'workflow/README') end - it "to #api" do - expect(get("/help/api/README")).to route_to('help#show', category: "api", file: "README") + it 'to #api' do + expect(get('/help/api/README')).to route_to('help#show', filepath: 'api/README') end - it "to #web_hooks" do - expect(get("/help/web_hooks/web_hooks")).to route_to('help#show', category: "web_hooks", file: "web_hooks") + it 'to #web_hooks' do + expect(get('/help/web_hooks/web_hooks')).to route_to('help#show', filepath: 'web_hooks/web_hooks') end - it "to #system_hooks" do - expect(get("/help/system_hooks/system_hooks")).to route_to('help#show', category: "system_hooks", file: "system_hooks") + it 'to #system_hooks' do + expect(get('/help/system_hooks/system_hooks')).to route_to('help#show', filepath: 'system_hooks/system_hooks') end - it "to #markdown" do - expect(get("/help/markdown/markdown")).to route_to('help#show',category: "markdown", file: "markdown") + it 'to #markdown' do + expect(get('/help/markdown/markdown')).to route_to('help#show',filepath: 'markdown/markdown') end - it "to #ssh" do - expect(get("/help/ssh/README")).to route_to('help#show', category: "ssh", file: "README") + it 'to #ssh' do + expect(get('/help/ssh/README')).to route_to('help#show', filepath: 'ssh/README') end - it "to #raketasks" do - expect(get("/help/raketasks/README")).to route_to('help#show', category: "raketasks", file: "README") + it 'to #raketasks' do + expect(get('/help/raketasks/README')).to route_to('help#show', filepath: 'raketasks/README') end end diff --git a/spec/services/archive_repository_service_spec.rb b/spec/services/archive_repository_service_spec.rb new file mode 100644 index 0000000000..f168a91397 --- /dev/null +++ b/spec/services/archive_repository_service_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' + +describe ArchiveRepositoryService do + let(:project) { create(:project) } + subject { ArchiveRepositoryService.new(project, "master", "zip") } + + describe "#execute" do + it "cleans old archives" do + expect(project.repository).to receive(:clean_old_archives) + + subject.execute(timeout: 0.0) + end + + context "when the repository doesn't have an archive file path" do + before do + allow(project.repository).to receive(:archive_file_path).and_return(nil) + end + + it "raises an error" do + expect { + subject.execute(timeout: 0.0) + }.to raise_error + end + end + + context "when the repository has an archive file path" do + let(:file_path) { "/archive.zip" } + let(:pid_file_path) { "/archive.zip.pid" } + + before do + allow(project.repository).to receive(:archive_file_path).and_return(file_path) + allow(project.repository).to receive(:archive_pid_file_path).and_return(pid_file_path) + end + + context "when the archive file already exists" do + before do + allow(File).to receive(:exist?).with(file_path).and_return(true) + end + + it "returns the file path" do + expect(subject.execute(timeout: 0.0)).to eq(file_path) + end + end + + context "when the archive file doesn't exist yet" do + before do + allow(File).to receive(:exist?).with(file_path).and_return(false) + allow(File).to receive(:exist?).with(pid_file_path).and_return(true) + end + + context "when the archive pid file doesn't exist yet" do + before do + allow(File).to receive(:exist?).with(pid_file_path).and_return(false) + end + + it "queues the RepositoryArchiveWorker" do + expect(RepositoryArchiveWorker).to receive(:perform_async) + + subject.execute(timeout: 0.0) + end + end + + context "when the archive pid file already exists" do + it "doesn't queue the RepositoryArchiveWorker" do + expect(RepositoryArchiveWorker).not_to receive(:perform_async) + + subject.execute(timeout: 0.0) + end + end + + context "when the archive file exists after a little while" do + before do + Thread.new do + sleep 0.1 + allow(File).to receive(:exist?).with(file_path).and_return(true) + end + end + + it "returns the file path" do + expect(subject.execute(timeout: 0.2)).to eq(file_path) + end + end + + context "when the archive file doesn't exist after the timeout" do + it "returns nil" do + expect(subject.execute(timeout: 0.0)).to eq(nil) + end + end + end + end + end +end + diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index eaec2198dc..53ccaa4fd6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -44,3 +44,5 @@ RSpec.configure do |config| TestEnv.init end end + +ActiveRecord::Migration.maintain_test_schema! diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index f869488d8d..44d70e741b 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -85,7 +85,7 @@ module TestEnv end # We must copy bare repositories because we will push to them. - system(*%W(git clone -q --bare #{factory_repo_path} #{factory_repo_path_bare})) + system(git_env, *%W(git clone -q --bare #{factory_repo_path} #{factory_repo_path_bare})) end def copy_repo(project) @@ -113,4 +113,10 @@ module TestEnv def factory_repo_name 'gitlab-test' end + + # Prevent developer git configurations from being persisted to test + # repositories + def git_env + {'GIT_TEMPLATE_DIR' => ''} + end end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 8a411b7720..60144be551 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -87,7 +87,7 @@ describe 'gitlab:app namespace rake task' do expect(tar_contents).to match('db/') expect(tar_contents).to match('uploads/') expect(tar_contents).to match('repositories/') - expect(tar_contents).not_to match(/^.{4,9}[rwx]/) + expect(tar_contents).not_to match(/^.{4,9}[rwx].*(db|uploads|repositories)\/$/) end it 'should delete temp directories' do @@ -98,4 +98,55 @@ describe 'gitlab:app namespace rake task' do expect(temp_dirs).to be_empty end end # backup_create task + + describe "Skipping items" do + def tars_glob + Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar')) + end + + before :all do + @origin_cd = Dir.pwd + + Rake::Task["gitlab:backup:db:create"].reenable + Rake::Task["gitlab:backup:repo:create"].reenable + Rake::Task["gitlab:backup:uploads:create"].reenable + + # Record the existing backup tars so we don't touch them + existing_tars = tars_glob + + # Redirect STDOUT and run the rake task + orig_stdout = $stdout + $stdout = StringIO.new + ENV["SKIP"] = "repositories" + run_rake_task('gitlab:backup:create') + $stdout = orig_stdout + + @backup_tar = (tars_glob - existing_tars).first + end + + after :all do + FileUtils.rm(@backup_tar) + Dir.chdir @origin_cd + end + + it "does not contain skipped item" do + tar_contents, exit_status = Gitlab::Popen.popen( + %W{tar -tvf #{@backup_tar} db uploads repositories} + ) + + expect(tar_contents).to match('db/') + expect(tar_contents).to match('uploads/') + expect(tar_contents).not_to match('repositories/') + end + + it 'does not invoke repositories restore' do + Rake::Task["gitlab:shell:setup"].stub invoke: true + allow($stdout).to receive :write + + expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke + expect(Rake::Task["gitlab:backup:repo:restore"]).not_to receive :invoke + expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke + expect { run_rake_task('gitlab:backup:restore') }.to_not raise_error + end + end end # gitlab:app namespace diff --git a/spec/workers/repository_archive_worker_spec.rb b/spec/workers/repository_archive_worker_spec.rb new file mode 100644 index 0000000000..c2362058cf --- /dev/null +++ b/spec/workers/repository_archive_worker_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe RepositoryArchiveWorker do + let(:project) { create(:project) } + subject { RepositoryArchiveWorker.new } + + before do + allow(Project).to receive(:find).and_return(project) + end + + describe "#perform" do + it "cleans old archives" do + expect(project.repository).to receive(:clean_old_archives) + + subject.perform(project.id, "master", "zip") + end + + context "when the repository doesn't have an archive file path" do + before do + allow(project.repository).to receive(:archive_file_path).and_return(nil) + end + + it "doesn't archive the repo" do + expect(project.repository).not_to receive(:archive_repo) + + subject.perform(project.id, "master", "zip") + end + end + + context "when the repository has an archive file path" do + let(:file_path) { "/archive.zip" } + let(:pid_file_path) { "/archive.zip.pid" } + + before do + allow(project.repository).to receive(:archive_file_path).and_return(file_path) + allow(project.repository).to receive(:archive_pid_file_path).and_return(pid_file_path) + end + + context "when the archive file already exists" do + before do + allow(File).to receive(:exist?).with(file_path).and_return(true) + end + + it "doesn't archive the repo" do + expect(project.repository).not_to receive(:archive_repo) + + subject.perform(project.id, "master", "zip") + end + end + + context "when the archive file doesn't exist yet" do + before do + allow(File).to receive(:exist?).with(file_path).and_return(false) + allow(File).to receive(:exist?).with(pid_file_path).and_return(true) + end + + context "when the archive pid file doesn't exist yet" do + before do + allow(File).to receive(:exist?).with(pid_file_path).and_return(false) + end + + it "archives the repo" do + expect(project.repository).to receive(:archive_repo) + + subject.perform(project.id, "master", "zip") + end + end + + context "when the archive pid file already exists" do + it "doesn't archive the repo" do + expect(project.repository).not_to receive(:archive_repo) + + subject.perform(project.id, "master", "zip") + end + end + end + end + end +end +