diff options
Diffstat (limited to 'lib')
73 files changed, 3104 insertions, 1484 deletions
diff --git a/lib/api.rb b/lib/api.rb deleted file mode 100644 index d9dce7c70cc..00000000000 --- a/lib/api.rb +++ /dev/null @@ -1,25 +0,0 @@ -Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file} - -module Gitlab - class API < Grape::API - version 'v3', using: :path - - rescue_from ActiveRecord::RecordNotFound do - rack_response({'message' => '404 Not found'}.to_json, 404) - end - - format :json - error_format :json - helpers APIHelpers - - mount Groups - mount Users - mount Projects - mount Issues - mount Milestones - mount Session - mount MergeRequests - mount Notes - mount Internal - end -end diff --git a/lib/api/api.rb b/lib/api/api.rb new file mode 100644 index 00000000000..c4c9f166db1 --- /dev/null +++ b/lib/api/api.rb @@ -0,0 +1,42 @@ +Dir["#{Rails.root}/lib/api/*.rb"].each {|file| require file} + +module API + class API < Grape::API + version 'v3', using: :path + + rescue_from ActiveRecord::RecordNotFound do + rack_response({'message' => '404 Not found'}.to_json, 404) + end + + rescue_from :all do |exception| + # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60 + # why is this not wrapped in something reusable? + trace = exception.backtrace + + message = "\n#{exception.class} (#{exception.message}):\n" + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) + message << " " << trace.join("\n ") + + API.logger.add Logger::FATAL, message + rack_response({'message' => '500 Internal Server Error'}, 500) + end + + format :json + helpers APIHelpers + + mount Groups + mount Users + mount Projects + mount Repositories + mount Issues + mount Milestones + mount Session + mount MergeRequests + mount Notes + mount Internal + mount SystemHooks + mount ProjectSnippets + mount DeployKeys + mount ProjectHooks + end +end diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb new file mode 100644 index 00000000000..55c947eb176 --- /dev/null +++ b/lib/api/deploy_keys.rb @@ -0,0 +1,84 @@ +module API + # Projects API + class DeployKeys < Grape::API + before { authenticate! } + + resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + end + + + # Get a specific project's keys + # + # Example Request: + # GET /projects/:id/keys + get ":id/keys" do + present user_project.deploy_keys, with: Entities::SSHKey + end + + # Get single key owned by currently authenticated user + # + # Example Request: + # GET /projects/:id/keys/:id + get ":id/keys/:key_id" do + key = user_project.deploy_keys.find params[:key_id] + present key, with: Entities::SSHKey + end + + # Add new ssh key to currently authenticated user + # If deploy key already exists - it will be joined to project + # but only if original one was is accessible by same user + # + # Parameters: + # key (required) - New SSH Key + # title (required) - New SSH Key's title + # Example Request: + # POST /projects/:id/keys + post ":id/keys" do + attrs = attributes_for_keys [:title, :key] + + if attrs[:key].present? + attrs[:key].strip! + + # check if key already exist in project + key = user_project.deploy_keys.find_by_key(attrs[:key]) + if key + present key, with: Entities::SSHKey + return + end + + # Check for available deploy keys in other projects + key = current_user.accessible_deploy_keys.find_by_key(attrs[:key]) + if key + user_project.deploy_keys << key + present key, with: Entities::SSHKey + return + end + end + + key = DeployKey.new attrs + + if key.valid? && user_project.deploy_keys << key + present key, with: Entities::SSHKey + else + not_found! + end + end + + # Delete existed ssh key of currently authenticated user + # + # Example Request: + # DELETE /projects/:id/keys/:id + delete ":id/keys/:key_id" do + key = user_project.deploy_keys.find params[:key_id] + key.destroy + end + end + end +end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index c1873d87b55..1f35e9ec5fc 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -1,46 +1,78 @@ -module Gitlab +module API module Entities class User < Grape::Entity expose :id, :username, :email, :name, :bio, :skype, :linkedin, :twitter, - :dark_scheme, :theme_id, :blocked, :created_at, :extern_uid, :provider + :theme_id, :color_scheme_id, :state, :created_at, :extern_uid, :provider + end + + class UserSafe < Grape::Entity + expose :name end class UserBasic < Grape::Entity - expose :id, :username, :email, :name, :blocked, :created_at + expose :id, :username, :email, :name, :state, :created_at end - class UserLogin < UserBasic + class UserLogin < User expose :private_token + expose :is_admin?, as: :is_admin + expose :can_create_group?, as: :can_create_group + expose :can_create_project?, as: :can_create_project + expose :can_create_team?, as: :can_create_team end class Hook < Grape::Entity expose :id, :url, :created_at end + class ForkedFromProject < Grape::Entity + expose :id + expose :name, :name_with_namespace + expose :path, :path_with_namespace + end + class Project < Grape::Entity - expose :id, :name, :description, :default_branch + expose :id, :description, :default_branch, :public, :ssh_url_to_repo, :http_url_to_repo, :web_url expose :owner, using: Entities::UserBasic - expose :private_flag, as: :private + expose :name, :name_with_namespace expose :path, :path_with_namespace - expose :issues_enabled, :merge_requests_enabled, :wall_enabled, :wiki_enabled, :created_at + expose :issues_enabled, :merge_requests_enabled, :wall_enabled, :wiki_enabled, :snippets_enabled, :created_at, :last_activity_at, :public expose :namespace + expose :forked_from_project, using: Entities::ForkedFromProject, :if => lambda{ | project, options | project.forked? } end class ProjectMember < UserBasic - expose :project_access, :as => :access_level do |user, options| + expose :project_access, as: :access_level do |user, options| options[:project].users_projects.find_by_user_id(user.id).project_access end end + class TeamMember < UserBasic + expose :permission, as: :access_level do |user, options| + options[:user_team].user_team_user_relationships.find_by_user_id(user.id).permission + end + end + + class TeamProject < Project + expose :greatest_access, as: :greatest_access_level do |project, options| + options[:user_team].user_team_project_relationships.find_by_project_id(project.id).greatest_access + end + end + class Group < Grape::Entity expose :id, :name, :path, :owner_id end - + class GroupDetail < Group expose :projects, using: Entities::Project end - + class GroupMember < UserBasic + expose :group_access, as: :access_level do |user, options| + options[:group].users_groups.find_by_user_id(user.id).group_access + end + end + class RepoObject < Grape::Entity expose :name, :commit expose :protected do |repo, options| @@ -63,7 +95,7 @@ module Gitlab class Milestone < Grape::Entity expose :id expose (:project_id) {|milestone| milestone.project.id} - expose :title, :description, :due_date, :closed, :updated_at, :created_at + expose :title, :description, :due_date, :state, :updated_at, :created_at end class Issue < Grape::Entity @@ -73,7 +105,7 @@ module Gitlab expose :label_list, as: :labels expose :milestone, using: Entities::Milestone expose :assignee, :author, using: Entities::UserBasic - expose :closed, :updated_at, :created_at + expose :state, :updated_at, :created_at end class SSHKey < Grape::Entity @@ -81,13 +113,15 @@ module Gitlab end class MergeRequest < Grape::Entity - expose :id, :target_branch, :source_branch, :project_id, :title, :closed, :merged + expose :id, :target_branch, :source_branch, :title, :state + expose :target_project_id, as: :project_id expose :author, :assignee, using: Entities::UserBasic end class Note < Grape::Entity expose :id expose :note, as: :body + expose :attachment_identifier, as: :attachment expose :author, using: Entities::UserBasic expose :created_at end @@ -96,5 +130,11 @@ module Gitlab expose :note expose :author, using: Entities::UserBasic end + + class Event < Grape::Entity + expose :title, :project_id, :action_name + expose :target_id, :target_type, :author_id + expose :data, :target_title + end end end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index a67caef0bc5..396554404af 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -1,9 +1,23 @@ -module Gitlab +module API # groups API class Groups < Grape::API before { authenticate! } resource :groups do + helpers do + def find_group(id) + group = Group.find(id) + if current_user.admin or current_user.groups.include? group + group + else + render_api_error!("403 Forbidden - #{current_user.username} lacks sufficient access to #{group.name}", 403) + end + end + def validate_access_level?(level) + Gitlab::Access.options_with_owner.values.include? level.to_i + end + end + # Get a groups list # # Example Request: @@ -20,12 +34,14 @@ module Gitlab # Create group. Available only for admin # # Parameters: - # name (required) - Name - # path (required) - Path + # name (required) - The name of the group + # path (required) - The path of the group # Example Request: # POST /groups post do authenticated_as_admin! + required_attributes! [:name, :path] + attrs = attributes_for_keys [:name, :path] @group = Group.new(attrs) @group.owner = current_user @@ -44,13 +60,79 @@ module Gitlab # Example Request: # GET /groups/:id get ":id" do + group = find_group(params[:id]) + present group, with: Entities::GroupDetail + end + + # Transfer a project to the Group namespace + # + # Parameters: + # id - group id + # project_id - project id + # Example Request: + # POST /groups/:id/projects/:project_id + post ":id/projects/:project_id" do + authenticated_as_admin! @group = Group.find(params[:id]) - if current_user.admin or current_user.groups.include? @group - present @group, with: Entities::GroupDetail + project = Project.find(params[:project_id]) + if project.transfer(@group) + present @group else not_found! end end + + # Get a list of group members viewable by the authenticated user. + # + # Example Request: + # GET /groups/:id/members + get ":id/members" do + group = find_group(params[:id]) + members = group.users_groups + users = (paginate members).collect(&:user) + present users, with: Entities::GroupMember, group: group + end + + # Add a user to the list of group members + # + # Parameters: + # id (required) - group id + # user_id (required) - the users id + # access_level (required) - Project access level + # Example Request: + # POST /groups/:id/members + post ":id/members" do + required_attributes! [:user_id, :access_level] + unless validate_access_level?(params[:access_level]) + render_api_error!("Wrong access level", 422) + end + group = find_group(params[:id]) + if group.users_groups.find_by_user_id(params[:user_id]) + render_api_error!("Already exists", 409) + end + group.add_users([params[:user_id]], params[:access_level]) + member = group.users_groups.find_by_user_id(params[:user_id]) + present member.user, with: Entities::GroupMember, group: group + end + + # Remove member. + # + # Parameters: + # id (required) - group id + # user_id (required) - the users id + # + # Example Request: + # DELETE /groups/:id/members/:user_id + delete ":id/members/:user_id" do + group = find_group(params[:id]) + member = group.users_groups.find_by_user_id(params[:user_id]) + if member.nil? + render_api_error!("404 Not Found - user_id:#{params[:user_id]} not a member of group #{group.name}",404) + else + member.destroy + end + end + end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 6bd8111c2b2..4f189f35196 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -1,16 +1,43 @@ -module Gitlab +module API module APIHelpers + PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN" + PRIVATE_TOKEN_PARAM = :private_token + SUDO_HEADER ="HTTP_SUDO" + SUDO_PARAM = :sudo + def current_user - @current_user ||= User.find_by_authentication_token(params[:private_token] || env["HTTP_PRIVATE_TOKEN"]) + @current_user ||= User.find_by_authentication_token(params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]) + identifier = sudo_identifier() + # If the sudo is the current user do nothing + if (identifier && !(@current_user.id == identifier || @current_user.username == identifier)) + render_api_error!('403 Forbidden: Must be admin to use sudo', 403) unless @current_user.is_admin? + begin + @current_user = User.by_username_or_id(identifier) + rescue => ex + not_found!("No user id or username for: #{identifier}") + end + not_found!("No user id or username for: #{identifier}") if current_user.nil? + end + @current_user + end + + def sudo_identifier() + identifier ||= params[SUDO_PARAM] ||= env[SUDO_HEADER] + # Regex for integers + if (!!(identifier =~ /^[0-9]+$/)) + identifier.to_i + else + identifier + end end def user_project - @project ||= find_project + @project ||= find_project(params[:id]) @project || not_found! end - def find_project - project = Project.find_by_id(params[:id]) || Project.find_with_namespace(params[:id]) + def find_project(id) + project = Project.find_by_id(id) || Project.find_with_namespace(id) if project && can?(current_user, :read_project, project) project @@ -41,6 +68,17 @@ module Gitlab abilities.allowed?(object, action, subject) end + # Checks the occurrences of required attributes, each attribute must be present in the params hash + # or a Bad Request error is invoked. + # + # Parameters: + # keys (required) - A hash consisting of keys that must be present + def required_attributes!(keys) + keys.each do |key| + bad_request!(key) unless params[key].present? + end + end + def attributes_for_keys(keys) attrs = {} keys.each do |key| @@ -55,6 +93,12 @@ module Gitlab render_api_error!('403 Forbidden', 403) end + def bad_request!(attribute) + message = ["400 (Bad request)"] + message << "\"" + attribute.to_s + "\" not given" + render_api_error!(message.join(' '), 400) + end + def not_found!(resource = nil) message = ["404"] message << resource if resource diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 3e5e3a478ba..79f8eb3a543 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -1,23 +1,45 @@ -module Gitlab +module API # Internal access API class Internal < Grape::API + + DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive } + PUSH_COMMANDS = %w{ git-receive-pack } + namespace 'internal' do # # Check if ssh key has access to project code # + # Params: + # key_id - SSH Key id + # project - project path with namespace + # action - git action (git-upload-pack or git-receive-pack) + # ref - branch name + # get "/allowed" do + # Check for *.wiki repositories. + # Strip out the .wiki from the pathname before finding the + # project. This applies the correct project permissions to + # the wiki repository as well. + project_path = params[:project] + project_path.gsub!(/\.wiki/,'') if project_path =~ /\.wiki/ + key = Key.find(params[:key_id]) - project = Project.find_with_namespace(params[:project]) + project = Project.find_with_namespace(project_path) git_cmd = params[:action] + return false unless project - if key.is_deploy_key - project == key.project && git_cmd == 'git-upload-pack' + + if key.is_a? DeployKey + key.projects.include?(project) && DOWNLOAD_COMMANDS.include?(git_cmd) else user = key.user + + return false if user.blocked? + action = case git_cmd - when 'git-upload-pack' + when *DOWNLOAD_COMMANDS then :download_code - when 'git-receive-pack' + when *PUSH_COMMANDS then if project.protected_branch?(params[:ref]) :push_code_to_protected_branches @@ -35,12 +57,14 @@ module Gitlab # get "/discover" do key = Key.find(params[:key_id]) - present key.user, with: Entities::User + present key.user, with: Entities::UserSafe end get "/check" do { - api_version: '3' + api_version: API.version, + gitlab_version: Gitlab::VERSION, + gitlab_rev: Gitlab::REVISION, } end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 4d832fbe593..a15203d1563 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -1,7 +1,8 @@ -module Gitlab +module API # Issues API class Issues < Grape::API before { authenticate! } + before { Thread.current[:current_user] = current_user } resource :issues do # Get currently authenticated user's issues @@ -48,6 +49,7 @@ module Gitlab # Example Request: # POST /projects/:id/issues post ":id/issues" do + required_attributes! [:title] attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id] attrs[:label_list] = params[:labels] if params[:labels].present? @issue = user_project.issues.new attrs @@ -69,16 +71,16 @@ module Gitlab # assignee_id (optional) - The ID of a user to assign issue # milestone_id (optional) - The ID of a milestone to assign issue # labels (optional) - The labels of an issue - # closed (optional) - The state of an issue (0 = false, 1 = true) + # state_event (optional) - The state event of an issue (close|reopen) # Example Request: # PUT /projects/:id/issues/:issue_id put ":id/issues/:issue_id" do @issue = user_project.issues.find(params[:issue_id]) authorize! :modify_issue, @issue - attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id, :closed] + attrs = attributes_for_keys [:title, :description, :assignee_id, :milestone_id, :state_event] attrs[:label_list] = params[:labels] if params[:labels].present? - IssueObserver.current_user = current_user + if @issue.update_attributes attrs present @issue, with: Entities::Issue else diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 470cd1e1c2d..d690f1d07e7 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -1,9 +1,28 @@ -module Gitlab +module API # MergeRequest API class MergeRequests < Grape::API before { authenticate! } + before { Thread.current[:current_user] = current_user } resource :projects do + helpers do + def handle_merge_request_errors!(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + elsif errors[:branch_conflict].any? + error!(errors[:branch_conflict], 422) + end + not_found! + end + + def not_fork?(target_project_id, user_project) + target_project_id.nil? || target_project_id == user_project.id.to_s + end + + def target_matches_fork(target_project_id,user_project) + user_project.forked? && user_project.forked_from_project.id.to_s == target_project_id + end + end # List merge requests # @@ -40,9 +59,10 @@ module Gitlab # # Parameters: # - # id (required) - The ID of a project + # id (required) - The ID of a project - this will be the source of the merge request # source_branch (required) - The source branch # target_branch (required) - The target branch + # target_project - The target project of the merge request defaults to the :id of the project # assignee_id - Assignee user ID # title (required) - Title of MR # @@ -51,16 +71,27 @@ module Gitlab # post ":id/merge_requests" do authorize! :write_merge_request, user_project - - attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title] + required_attributes! [:source_branch, :target_branch, :title] + attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :target_project_id] merge_request = user_project.merge_requests.new(attrs) merge_request.author = current_user + merge_request.source_project = user_project + target_project_id = attrs[:target_project_id] + if not_fork?(target_project_id, user_project) + merge_request.target_project = user_project + else + if target_matches_fork(target_project_id,user_project) + merge_request.target_project = Project.find_by_id(attrs[:target_project_id]) + else + render_api_error!('(Bad Request) Specified target project that is not the source project, or the source fork of the project.', 400) + end + end if merge_request.save merge_request.reload_code present merge_request, with: Entities::MergeRequest else - not_found! + handle_merge_request_errors! merge_request.errors end end @@ -73,12 +104,12 @@ module Gitlab # target_branch - The target branch # assignee_id - Assignee user ID # title - Title of MR - # closed - Status of MR. true - closed + # state_event - Status of MR. (close|reopen|merge) # Example: # PUT /projects/:id/merge_request/:merge_request_id # put ":id/merge_request/:merge_request_id" do - attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :closed] + attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :state_event] merge_request = user_project.merge_requests.find(params[:merge_request_id]) authorize! :modify_merge_request, merge_request @@ -88,7 +119,7 @@ module Gitlab merge_request.mark_as_unchecked present merge_request, with: Entities::MergeRequest else - not_found! + handle_merge_request_errors! merge_request.errors end end @@ -102,6 +133,8 @@ module Gitlab # POST /projects/:id/merge_request/:merge_request_id/comments # post ":id/merge_request/:merge_request_id/comments" do + required_attributes! [:note] + merge_request = user_project.merge_requests.find(params[:merge_request_id]) note = merge_request.notes.new(note: params[:note], project_id: user_project.id) note.author = current_user diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index 6aca9d01b09..aee12e7dc40 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -1,4 +1,4 @@ -module Gitlab +module API # Milestones API class Milestones < Grape::API before { authenticate! } @@ -41,6 +41,7 @@ module Gitlab # POST /projects/:id/milestones post ":id/milestones" do authorize! :admin_milestone, user_project + required_attributes! [:title] attrs = attributes_for_keys [:title, :description, :due_date] @milestone = user_project.milestones.new attrs @@ -59,14 +60,14 @@ module Gitlab # title (optional) - The title of a milestone # description (optional) - The description of a milestone # due_date (optional) - The due date of a milestone - # closed (optional) - The status of the milestone + # state_event (optional) - The state event of the milestone (close|activate) # Example Request: # PUT /projects/:id/milestones/:milestone_id put ":id/milestones/:milestone_id" do authorize! :admin_milestone, user_project @milestone = user_project.milestones.find(params[:milestone_id]) - attrs = attributes_for_keys [:title, :description, :due_date, :closed] + attrs = attributes_for_keys [:title, :description, :due_date, :state_event] if @milestone.update_attributes attrs present @milestone, with: Entities::Milestone else diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 70344d6e381..cb2bc764476 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -1,4 +1,4 @@ -module Gitlab +module API # Notes API class Notes < Grape::API before { authenticate! } @@ -14,6 +14,10 @@ module Gitlab # GET /projects/:id/notes get ":id/notes" do @notes = user_project.notes.common + + # Get recent notes if recent = true + @notes = @notes.order('id DESC') if params[:recent] + present paginate(@notes), with: Entities::Note end @@ -37,12 +41,16 @@ module Gitlab # Example Request: # POST /projects/:id/notes post ":id/notes" do + required_attributes! [:body] + @note = user_project.notes.new(note: params[:body]) @note.author = current_user if @note.save present @note, with: Entities::Note else + # :note is exposed as :body, but :note is set on error + bad_request!(:note) if @note.errors[:note].any? not_found! end end @@ -89,6 +97,8 @@ module Gitlab # POST /projects/:id/issues/:noteable_id/notes # POST /projects/:id/snippets/:noteable_id/notes post ":id/#{noteables_str}/:#{noteable_id_str}/notes" do + required_attributes! [:body] + @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) @note = @noteable.notes.new(note: params[:body]) @note.author = current_user diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb new file mode 100644 index 00000000000..28501256795 --- /dev/null +++ b/lib/api/project_hooks.rb @@ -0,0 +1,108 @@ +module API + # Projects API + class ProjectHooks < Grape::API + before { authenticate! } + + resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + end + + # Get project hooks + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id/hooks + get ":id/hooks" do + authorize! :admin_project, user_project + @hooks = paginate user_project.hooks + present @hooks, with: Entities::Hook + end + + # Get a project hook + # + # Parameters: + # id (required) - The ID of a project + # hook_id (required) - The ID of a project hook + # Example Request: + # GET /projects/:id/hooks/:hook_id + get ":id/hooks/:hook_id" do + authorize! :admin_project, user_project + @hook = user_project.hooks.find(params[:hook_id]) + present @hook, with: Entities::Hook + end + + + # Add hook to project + # + # Parameters: + # id (required) - The ID of a project + # url (required) - The hook URL + # Example Request: + # POST /projects/:id/hooks + post ":id/hooks" do + authorize! :admin_project, user_project + required_attributes! [:url] + + @hook = user_project.hooks.new({"url" => params[:url]}) + if @hook.save + present @hook, with: Entities::Hook + else + if @hook.errors[:url].present? + error!("Invalid url given", 422) + end + not_found! + end + end + + # Update an existing project hook + # + # Parameters: + # id (required) - The ID of a project + # hook_id (required) - The ID of a project hook + # url (required) - The hook URL + # Example Request: + # PUT /projects/:id/hooks/:hook_id + put ":id/hooks/:hook_id" do + @hook = user_project.hooks.find(params[:hook_id]) + authorize! :admin_project, user_project + required_attributes! [:url] + + attrs = attributes_for_keys [:url] + if @hook.update_attributes attrs + present @hook, with: Entities::Hook + else + if @hook.errors[:url].present? + error!("Invalid url given", 422) + end + not_found! + end + end + + # Deletes project hook. This is an idempotent function. + # + # Parameters: + # id (required) - The ID of a project + # hook_id (required) - The ID of hook to delete + # Example Request: + # DELETE /projects/:id/hooks/:hook_id + delete ":id/hooks/:hook_id" do + authorize! :admin_project, user_project + required_attributes! [:hook_id] + + begin + @hook = ProjectHook.find(params[:hook_id]) + @hook.destroy + rescue + # ProjectHook can raise Error if hook_id not found + end + end + end + end +end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb new file mode 100644 index 00000000000..bee6544ea3d --- /dev/null +++ b/lib/api/project_snippets.rb @@ -0,0 +1,123 @@ +module API + # Projects API + class ProjectSnippets < Grape::API + before { authenticate! } + + resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + end + + # Get a project snippets + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id/snippets + get ":id/snippets" do + present paginate(user_project.snippets), with: Entities::ProjectSnippet + end + + # Get a project snippet + # + # Parameters: + # id (required) - The ID of a project + # snippet_id (required) - The ID of a project snippet + # Example Request: + # GET /projects/:id/snippets/:snippet_id + get ":id/snippets/:snippet_id" do + @snippet = user_project.snippets.find(params[:snippet_id]) + present @snippet, with: Entities::ProjectSnippet + end + + # Create a new project snippet + # + # Parameters: + # id (required) - The ID of a project + # title (required) - The title of a snippet + # file_name (required) - The name of a snippet file + # lifetime (optional) - The expiration date of a snippet + # code (required) - The content of a snippet + # Example Request: + # POST /projects/:id/snippets + post ":id/snippets" do + authorize! :write_project_snippet, user_project + required_attributes! [:title, :file_name, :code] + + attrs = attributes_for_keys [:title, :file_name] + attrs[:expires_at] = params[:lifetime] if params[:lifetime].present? + attrs[:content] = params[:code] if params[:code].present? + @snippet = user_project.snippets.new attrs + @snippet.author = current_user + + if @snippet.save + present @snippet, with: Entities::ProjectSnippet + else + not_found! + end + end + + # Update an existing project snippet + # + # Parameters: + # id (required) - The ID of a project + # snippet_id (required) - The ID of a project snippet + # title (optional) - The title of a snippet + # file_name (optional) - The name of a snippet file + # lifetime (optional) - The expiration date of a snippet + # code (optional) - The content of a snippet + # Example Request: + # PUT /projects/:id/snippets/:snippet_id + put ":id/snippets/:snippet_id" do + @snippet = user_project.snippets.find(params[:snippet_id]) + authorize! :modify_project_snippet, @snippet + + attrs = attributes_for_keys [:title, :file_name] + attrs[:expires_at] = params[:lifetime] if params[:lifetime].present? + attrs[:content] = params[:code] if params[:code].present? + + if @snippet.update_attributes attrs + present @snippet, with: Entities::ProjectSnippet + else + not_found! + end + end + + # Delete a project snippet + # + # Parameters: + # id (required) - The ID of a project + # snippet_id (required) - The ID of a project snippet + # Example Request: + # DELETE /projects/:id/snippets/:snippet_id + delete ":id/snippets/:snippet_id" do + begin + @snippet = user_project.snippets.find(params[:snippet_id]) + authorize! :modify_project_snippet, @snippet + @snippet.destroy + rescue + end + end + + # Get a raw project snippet + # + # Parameters: + # id (required) - The ID of a project + # snippet_id (required) - The ID of a project snippet + # Example Request: + # GET /projects/:id/snippets/:snippet_id/raw + get ":id/snippets/:snippet_id/raw" do + @snippet = user_project.snippets.find(params[:snippet_id]) + + env['api.format'] = :txt + content_type 'text/plain' + present @snippet.content + end + end + end +end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index d416121a78a..cf357b23c40 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -1,9 +1,18 @@ -module Gitlab +module API # Projects API class Projects < Grape::API before { authenticate! } resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + end + # Get a projects list for authenticated user # # Example Request: @@ -13,6 +22,15 @@ module Gitlab present @projects, with: Entities::Project end + # Get an owned projects list for authenticated user + # + # Example Request: + # GET /projects/owned + get '/owned' do + @projects = paginate current_user.owned_projects + present @projects, with: Entities::Project + end + # Get a single project # # Parameters: @@ -23,32 +41,128 @@ module Gitlab present user_project, with: Entities::Project end + # Get a single project events + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id + 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) + + present events, with: Entities::Event + end + # Create new project # # Parameters: # name (required) - name for new project # description (optional) - short project description # default_branch (optional) - 'master' by default - # issues_enabled (optional) - enabled by default - # wall_enabled (optional) - enabled by default - # merge_requests_enabled (optional) - enabled by default - # wiki_enabled (optional) - enabled by default + # issues_enabled (optional) + # wall_enabled (optional) + # merge_requests_enabled (optional) + # wiki_enabled (optional) + # snippets_enabled (optional) + # namespace_id (optional) - defaults to user namespace + # public (optional) - false by default # Example Request # POST /projects post do + required_attributes! [:name] attrs = attributes_for_keys [:name, - :description, - :default_branch, - :issues_enabled, - :wall_enabled, - :merge_requests_enabled, - :wiki_enabled] + :path, + :description, + :default_branch, + :issues_enabled, + :wall_enabled, + :merge_requests_enabled, + :wiki_enabled, + :snippets_enabled, + :namespace_id, + :public] @project = ::Projects::CreateContext.new(current_user, attrs).execute if @project.saved? present @project, with: Entities::Project else + if @project.errors[:limit_reached].present? + error!(@project.errors[:limit_reached], 403) + end + not_found! + end + end + + # Create new project for a specified user. Only available to admin users. + # + # Parameters: + # user_id (required) - The ID of a user + # name (required) - name for new project + # description (optional) - short project description + # default_branch (optional) - 'master' by default + # issues_enabled (optional) + # wall_enabled (optional) + # merge_requests_enabled (optional) + # wiki_enabled (optional) + # snippets_enabled (optional) + # public (optional) + # Example Request + # POST /projects/user/:user_id + post "user/:user_id" do + authenticated_as_admin! + user = User.find(params[:user_id]) + attrs = attributes_for_keys [:name, + :description, + :default_branch, + :issues_enabled, + :wall_enabled, + :merge_requests_enabled, + :wiki_enabled, + :snippets_enabled, + :public] + @project = ::Projects::CreateContext.new(user, attrs).execute + if @project.saved? + present @project, with: Entities::Project + else + not_found! + end + end + + + # Mark this project as forked from another + # + # Parameters: + # id: (required) - The ID of the project being marked as a fork + # forked_from_id: (required) - The ID of the project it was forked from + # Example Request: + # POST /projects/:id/fork/:forked_from_id + post ":id/fork/:forked_from_id" do + authenticated_as_admin! + forked_from_project = find_project(params[:forked_from_id]) + unless forked_from_project.nil? + if user_project.forked_from_project.nil? + user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id) + else + render_api_error!("Project already forked", 409) + end + else not_found! end + + end + + # Remove a forked_from relationship + # + # Parameters: + # id: (required) - The ID of the project being marked as a fork + # Example Request: + # DELETE /projects/:id/fork + delete ":id/fork" do + authenticated_as_admin! + unless user_project.forked_project_link.nil? + user_project.forked_project_link.destroy + end end # Get a project team members @@ -89,16 +203,22 @@ module Gitlab # POST /projects/:id/members post ":id/members" do authorize! :admin_project, user_project - users_project = user_project.users_projects.new( - user_id: params[:user_id], - project_access: params[:access_level] - ) + required_attributes! [:user_id, :access_level] + + # either the user is already a team member or a new one + team_member = user_project.team_member_by_id(params[:user_id]) + if team_member.nil? + team_member = user_project.users_projects.new( + user_id: params[:user_id], + project_access: params[:access_level] + ) + end - if users_project.save - @member = users_project.user + if team_member.save + @member = team_member.user present @member, with: Entities::ProjectMember, project: user_project else - not_found! + handle_project_member_errors team_member.errors end end @@ -112,13 +232,16 @@ module Gitlab # PUT /projects/:id/members/:user_id put ":id/members/:user_id" do authorize! :admin_project, user_project - users_project = user_project.users_projects.find_by_user_id params[:user_id] + required_attributes! [:access_level] + + team_member = user_project.users_projects.find_by_user_id(params[:user_id]) + not_found!("User can not be found") if team_member.nil? - if users_project.update_attributes(project_access: params[:access_level]) - @member = users_project.user + if team_member.update_attributes(project_access: params[:access_level]) + @member = team_member.user present @member, with: Entities::ProjectMember, project: user_project else - not_found! + handle_project_member_errors team_member.errors end end @@ -131,297 +254,27 @@ module Gitlab # DELETE /projects/:id/members/:user_id delete ":id/members/:user_id" do authorize! :admin_project, user_project - users_project = user_project.users_projects.find_by_user_id params[:user_id] - users_project.destroy - end - - # Get project hooks - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/hooks - get ":id/hooks" do - authorize! :admin_project, user_project - @hooks = paginate user_project.hooks - present @hooks, with: Entities::Hook - end - - # Get a project hook - # - # Parameters: - # id (required) - The ID of a project - # hook_id (required) - The ID of a project hook - # Example Request: - # GET /projects/:id/hooks/:hook_id - get ":id/hooks/:hook_id" do - @hook = user_project.hooks.find(params[:hook_id]) - present @hook, with: Entities::Hook - end - - - # Add hook to project - # - # Parameters: - # id (required) - The ID of a project - # url (required) - The hook URL - # Example Request: - # POST /projects/:id/hooks - post ":id/hooks" do - authorize! :admin_project, user_project - @hook = user_project.hooks.new({"url" => params[:url]}) - if @hook.save - present @hook, with: Entities::Hook + team_member = user_project.users_projects.find_by_user_id(params[:user_id]) + unless team_member.nil? + team_member.destroy else - error!({'message' => '404 Not found'}, 404) + {message: "Access revoked", id: params[:user_id].to_i} end end - # Update an existing project hook + # search for projects current_user has access to # # Parameters: - # id (required) - The ID of a project - # hook_id (required) - The ID of a project hook - # url (required) - The hook URL + # query (required) - A string contained in the project name + # per_page (optional) - number of projects to return per page + # page (optional) - the page to retrieve # Example Request: - # PUT /projects/:id/hooks/:hook_id - put ":id/hooks/:hook_id" do - @hook = user_project.hooks.find(params[:hook_id]) - authorize! :admin_project, user_project - - attrs = attributes_for_keys [:url] - - if @hook.update_attributes attrs - present @hook, with: Entities::Hook - else - not_found! - end + # GET /projects/search/:query + get "/search/:query" do + ids = current_user.authorized_projects.map(&:id) + projects = Project.where("(id in (?) OR public = true) AND (name LIKE (?))", ids, "%#{params[:query]}%") + present paginate(projects), with: Entities::Project end - - # Delete project hook - # - # Parameters: - # id (required) - The ID of a project - # hook_id (required) - The ID of hook to delete - # Example Request: - # DELETE /projects/:id/hooks - delete ":id/hooks" do - authorize! :admin_project, user_project - @hook = user_project.hooks.find(params[:hook_id]) - @hook.destroy - end - - # Get a project repository branches - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/repository/branches - get ":id/repository/branches" do - present user_project.repo.heads.sort_by(&:name), with: Entities::RepoObject, project: user_project - end - - # Get a single branch - # - # Parameters: - # id (required) - The ID of a project - # branch (required) - The name of the branch - # Example Request: - # GET /projects/:id/repository/branches/:branch - get ":id/repository/branches/:branch" do - @branch = user_project.repo.heads.find { |item| item.name == params[:branch] } - not_found!("Branch does not exist") if @branch.nil? - present @branch, with: Entities::RepoObject, project: user_project - end - - # Protect a single branch - # - # Parameters: - # id (required) - The ID of a project - # branch (required) - The name of the branch - # Example Request: - # PUT /projects/:id/repository/branches/:branch/protect - put ":id/repository/branches/:branch/protect" do - @branch = user_project.repo.heads.find { |item| item.name == params[:branch] } - protected = user_project.protected_branches.find_by_name(@branch.name) - - unless protected - user_project.protected_branches.create(:name => @branch.name) - end - - present @branch, with: Entities::RepoObject, project: user_project - end - - # Unprotect a single branch - # - # Parameters: - # id (required) - The ID of a project - # branch (required) - The name of the branch - # Example Request: - # PUT /projects/:id/repository/branches/:branch/unprotect - put ":id/repository/branches/:branch/unprotect" do - @branch = user_project.repo.heads.find { |item| item.name == params[:branch] } - protected = user_project.protected_branches.find_by_name(@branch.name) - - if protected - protected.destroy - end - - present @branch, with: Entities::RepoObject, project: user_project - end - - # Get a project repository tags - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/repository/tags - get ":id/repository/tags" do - present user_project.repo.tags.sort_by(&:name).reverse, with: Entities::RepoObject - end - - # Get a project repository commits - # - # Parameters: - # id (required) - The ID of a project - # ref_name (optional) - The name of a repository branch or tag - # Example Request: - # GET /projects/:id/repository/commits - get ":id/repository/commits" do - authorize! :download_code, user_project - - page = params[:page] || 0 - per_page = params[:per_page] || 20 - ref = params[:ref_name] || user_project.try(:default_branch) || 'master' - - commits = user_project.repository.commits(ref, nil, per_page, page * per_page) - present CommitDecorator.decorate(commits), with: Entities::RepoCommit - end - - # Get a project snippets - # - # Parameters: - # id (required) - The ID of a project - # Example Request: - # GET /projects/:id/snippets - get ":id/snippets" do - present paginate(user_project.snippets), with: Entities::ProjectSnippet - end - - # Get a project snippet - # - # Parameters: - # id (required) - The ID of a project - # snippet_id (required) - The ID of a project snippet - # Example Request: - # GET /projects/:id/snippets/:snippet_id - get ":id/snippets/:snippet_id" do - @snippet = user_project.snippets.find(params[:snippet_id]) - present @snippet, with: Entities::ProjectSnippet - end - - # Create a new project snippet - # - # Parameters: - # id (required) - The ID of a project - # title (required) - The title of a snippet - # file_name (required) - The name of a snippet file - # lifetime (optional) - The expiration date of a snippet - # code (required) - The content of a snippet - # Example Request: - # POST /projects/:id/snippets - post ":id/snippets" do - authorize! :write_snippet, user_project - - attrs = attributes_for_keys [:title, :file_name] - attrs[:expires_at] = params[:lifetime] if params[:lifetime].present? - attrs[:content] = params[:code] if params[:code].present? - @snippet = user_project.snippets.new attrs - @snippet.author = current_user - - if @snippet.save - present @snippet, with: Entities::ProjectSnippet - else - not_found! - end - end - - # Update an existing project snippet - # - # Parameters: - # id (required) - The ID of a project - # snippet_id (required) - The ID of a project snippet - # title (optional) - The title of a snippet - # file_name (optional) - The name of a snippet file - # lifetime (optional) - The expiration date of a snippet - # code (optional) - The content of a snippet - # Example Request: - # PUT /projects/:id/snippets/:snippet_id - put ":id/snippets/:snippet_id" do - @snippet = user_project.snippets.find(params[:snippet_id]) - authorize! :modify_snippet, @snippet - - attrs = attributes_for_keys [:title, :file_name] - attrs[:expires_at] = params[:lifetime] if params[:lifetime].present? - attrs[:content] = params[:code] if params[:code].present? - - if @snippet.update_attributes attrs - present @snippet, with: Entities::ProjectSnippet - else - not_found! - end - end - - # Delete a project snippet - # - # Parameters: - # id (required) - The ID of a project - # snippet_id (required) - The ID of a project snippet - # Example Request: - # DELETE /projects/:id/snippets/:snippet_id - delete ":id/snippets/:snippet_id" do - @snippet = user_project.snippets.find(params[:snippet_id]) - authorize! :modify_snippet, @snippet - - @snippet.destroy - end - - # Get a raw project snippet - # - # Parameters: - # id (required) - The ID of a project - # snippet_id (required) - The ID of a project snippet - # Example Request: - # GET /projects/:id/snippets/:snippet_id/raw - get ":id/snippets/:snippet_id/raw" do - @snippet = user_project.snippets.find(params[:snippet_id]) - content_type 'text/plain' - present @snippet.content - end - - # Get a raw file contents - # - # Parameters: - # id (required) - The ID of a project - # sha (required) - The commit or branch name - # filepath (required) - The path to the file to display - # Example Request: - # GET /projects/:id/repository/commits/:sha/blob - get ":id/repository/commits/:sha/blob" do - authorize! :download_code, user_project - - ref = params[:sha] - - commit = user_project.repository.commit ref - not_found! "Commit" unless commit - - tree = Tree.new commit.tree, ref, params[:filepath] - not_found! "File" unless tree.try(:tree) - - content_type tree.mime_type - present tree.data - end - end end end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb new file mode 100644 index 00000000000..fef32d3a2fe --- /dev/null +++ b/lib/api/repositories.rb @@ -0,0 +1,190 @@ +module API + # Projects API + class Repositories < Grape::API + before { authenticate! } + + resource :projects do + helpers do + def handle_project_member_errors(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + end + not_found! + end + end + + # Get a project repository branches + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id/repository/branches + get ":id/repository/branches" do + present user_project.repo.heads.sort_by(&:name), with: Entities::RepoObject, project: user_project + end + + # Get a single branch + # + # Parameters: + # id (required) - The ID of a project + # branch (required) - The name of the branch + # Example Request: + # GET /projects/:id/repository/branches/:branch + get ":id/repository/branches/:branch" do + @branch = user_project.repo.heads.find { |item| item.name == params[:branch] } + not_found!("Branch does not exist") if @branch.nil? + present @branch, with: Entities::RepoObject, project: user_project + end + + # Protect a single branch + # + # Parameters: + # id (required) - The ID of a project + # branch (required) - The name of the branch + # Example Request: + # PUT /projects/:id/repository/branches/:branch/protect + put ":id/repository/branches/:branch/protect" do + @branch = user_project.repo.heads.find { |item| item.name == params[:branch] } + not_found! unless @branch + protected = user_project.protected_branches.find_by_name(@branch.name) + + unless protected + user_project.protected_branches.create(name: @branch.name) + end + + present @branch, with: Entities::RepoObject, project: user_project + end + + # Unprotect a single branch + # + # Parameters: + # id (required) - The ID of a project + # branch (required) - The name of the branch + # Example Request: + # PUT /projects/:id/repository/branches/:branch/unprotect + put ":id/repository/branches/:branch/unprotect" do + @branch = user_project.repo.heads.find { |item| item.name == params[:branch] } + not_found! unless @branch + protected = user_project.protected_branches.find_by_name(@branch.name) + + if protected + protected.destroy + end + + present @branch, with: Entities::RepoObject, project: user_project + end + + # Get a project repository tags + # + # Parameters: + # id (required) - The ID of a project + # Example Request: + # GET /projects/:id/repository/tags + get ":id/repository/tags" do + present user_project.repo.tags.sort_by(&:name).reverse, with: Entities::RepoObject + end + + # Get a project repository commits + # + # Parameters: + # id (required) - The ID of a project + # ref_name (optional) - The name of a repository branch or tag, if not given the default branch is used + # Example Request: + # GET /projects/:id/repository/commits + get ":id/repository/commits" do + authorize! :download_code, user_project + + page = (params[:page] || 0).to_i + per_page = (params[:per_page] || 20).to_i + ref = params[:ref_name] || user_project.try(:default_branch) || 'master' + + commits = user_project.repository.commits(ref, nil, per_page, page * per_page) + present commits, with: Entities::RepoCommit + end + + # Get a specific commit of a project + # + # Parameters: + # id (required) - The ID of a project + # sha (required) - The commit hash or name of a repository branch or tag + # Example Request: + # GET /projects/:id/repository/commits/:sha + get ":id/repository/commits/:sha" do + authorize! :download_code, user_project + sha = params[:sha] + commit = user_project.repository.commit(sha) + not_found! "Commit" unless commit + present commit, with: Entities::RepoCommit + end + + # Get the diff for a specific commit of a project + # + # Parameters: + # id (required) - The ID of a project + # sha (required) - The commit or branch name + # Example Request: + # GET /projects/:id/repository/commits/:sha/diff + get ":id/repository/commits/:sha/diff" do + authorize! :download_code, user_project + sha = params[:sha] + result = CommitLoadContext.new(user_project, current_user, {id: sha}).execute + not_found! "Commit" unless result[:commit] + result[:commit].diffs + end + + # Get a project repository tree + # + # Parameters: + # id (required) - The ID of a project + # ref_name (optional) - The name of a repository branch or tag, if not given the default branch is used + # Example Request: + # GET /projects/:id/repository/tree + get ":id/repository/tree" do + authorize! :download_code, user_project + + ref = params[:ref_name] || user_project.try(:default_branch) || 'master' + path = params[:path] || nil + + commit = user_project.repository.commit(ref) + tree = Tree.new(user_project.repository, commit.id, ref, path) + + trees = [] + + %w(trees blobs submodules).each do |type| + trees += tree.send(type).map { |t| { name: t.name, type: type.singularize, mode: t.mode, id: t.id } } + end + + trees + end + + # Get a raw file contents + # + # Parameters: + # id (required) - The ID of a project + # sha (required) - The commit or branch name + # filepath (required) - The path to the file to display + # Example Request: + # GET /projects/:id/repository/blobs/:sha + get [ ":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob" ] do + authorize! :download_code, user_project + required_attributes! [:filepath] + + ref = params[:sha] + + repo = user_project.repository + + commit = repo.commit(ref) + not_found! "Commit" unless commit + + blob = Gitlab::Git::Blob.new(repo, commit.id, ref, params[:filepath]) + not_found! "File" unless blob.exists? + + env['api.format'] = :txt + + content_type blob.mime_type + present blob.data + end + end + end +end + diff --git a/lib/api/session.rb b/lib/api/session.rb index b4050160ae4..cc646895914 100644 --- a/lib/api/session.rb +++ b/lib/api/session.rb @@ -1,20 +1,21 @@ -module Gitlab +module API # Users API class Session < Grape::API # Login to get token # + # Parameters: + # login (*required) - user login + # email (*required) - user email + # password (required) - user password + # # Example Request: # POST /session post "/session" do - resource = User.find_for_database_authentication(email: params[:email]) - - return unauthorized! unless resource + auth = Gitlab::Auth.new + user = auth.find(params[:email] || params[:login], params[:password]) - if resource.valid_password?(params[:password]) - present resource, with: Entities::UserLogin - else - unauthorized! - end + return unauthorized! unless user + present user, with: Entities::UserLogin end end end diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb new file mode 100644 index 00000000000..3e239c5afe7 --- /dev/null +++ b/lib/api/system_hooks.rb @@ -0,0 +1,70 @@ +module API + # Hooks API + class SystemHooks < Grape::API + before { + authenticate! + authenticated_as_admin! + } + + resource :hooks do + # Get the list of system hooks + # + # Example Request: + # GET /hooks + get do + @hooks = SystemHook.all + present @hooks, with: Entities::Hook + end + + # Create new system hook + # + # Parameters: + # url (required) - url for system hook + # Example Request + # POST /hooks + post do + attrs = attributes_for_keys [:url] + required_attributes! [:url] + @hook = SystemHook.new attrs + if @hook.save + present @hook, with: Entities::Hook + else + not_found! + end + end + + # Test a hook + # + # Example Request + # GET /hooks/:id + get ":id" do + @hook = SystemHook.find(params[:id]) + data = { + event_name: "project_create", + name: "Ruby", + path: "ruby", + project_id: 1, + owner_name: "Someone", + owner_email: "example@gitlabhq.com" + } + @hook.execute(data) + data + end + + # Delete a hook. This is an idempotent function. + # + # Parameters: + # id (required) - ID of the hook + # Example Request: + # DELETE /hooks/:id + delete ":id" do + begin + @hook = SystemHook.find(params[:id]) + @hook.destroy + rescue + # SystemHook raises an Error if no hook with id found + end + end + end + end +end diff --git a/lib/api/users.rb b/lib/api/users.rb index 7ea90c75e9e..00dc2311ffd 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -1,4 +1,4 @@ -module Gitlab +module API # Users API class Users < Grape::API before { authenticate! } @@ -9,7 +9,10 @@ module Gitlab # Example Request: # GET /users get do - @users = paginate User + @users = User.scoped + @users = @users.active if params[:active].present? + @users = @users.search(params[:search]) if params[:search].present? + @users = paginate @users present @users, with: Entities::User end @@ -41,8 +44,9 @@ module Gitlab # POST /users post do authenticated_as_admin! + required_attributes! [:email, :password, :name, :username] attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :extern_uid, :provider, :bio] - user = User.new attrs, as: :admin + user = User.build_user(attrs, as: :admin) if user.save present user, with: Entities::User else @@ -59,7 +63,7 @@ module Gitlab # skype - Skype ID # linkedin - Linkedin # twitter - Twitter account - # projects_limit - Limit projects wich user can create + # projects_limit - Limit projects each user can create # extern_uid - External authentication provider UID # provider - External provider # bio - Bio @@ -67,16 +71,38 @@ module Gitlab # PUT /users/:id put ":id" do authenticated_as_admin! + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :extern_uid, :provider, :bio] - user = User.find_by_id(params[:id]) + user = User.find(params[:id]) + not_found!("User not found") unless user - if user && user.update_attributes(attrs) + if user.update_attributes(attrs) present user, with: Entities::User else not_found! end end + # Add ssh key to a specified user. Only available to admin users. + # + # Parameters: + # id (required) - The ID of a user + # key (required) - New SSH Key + # title (required) - New SSH Key's title + # Example Request: + # POST /users/:id/keys + post ":id/keys" do + authenticated_as_admin! + user = User.find(params[:id]) + attrs = attributes_for_keys [:title, :key] + key = user.keys.new attrs + if key.save + present key, with: Entities::SSHKey + else + not_found! + end + end + # Delete user. Available only for admin # # Example Request: @@ -99,7 +125,7 @@ module Gitlab # Example Request: # GET /user get do - present @current_user, with: Entities::User + present @current_user, with: Entities::UserLogin end # Get currently authenticated user's keys @@ -127,6 +153,8 @@ module Gitlab # Example Request: # POST /user/keys post "keys" do + required_attributes! [:title, :key] + attrs = attributes_for_keys [:title, :key] key = current_user.keys.new attrs if key.save @@ -136,15 +164,18 @@ module Gitlab end end - # Delete existed ssh key of currently authenticated user + # Delete existing ssh key of currently authenticated user # # Parameters: # id (required) - SSH Key ID # Example Request: # DELETE /user/keys/:id delete "keys/:id" do - key = current_user.keys.find params[:id] - key.delete + begin + key = current_user.keys.find params[:id] + key.destroy + rescue + end end end end diff --git a/lib/backup/database.rb b/lib/backup/database.rb new file mode 100644 index 00000000000..c4fb2e2e159 --- /dev/null +++ b/lib/backup/database.rb @@ -0,0 +1,58 @@ +require 'yaml' + +module Backup + class Database + attr_reader :config, :db_dir + + def initialize + @config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env] + @db_dir = File.join(Gitlab.config.backup.path, 'db') + FileUtils.mkdir_p(@db_dir) unless Dir.exists?(@db_dir) + end + + def dump + case config["adapter"] + when /^mysql/ then + system("mysqldump #{mysql_args} #{config['database']} > #{db_file_name}") + when "postgresql" then + pg_env + system("pg_dump #{config['database']} > #{db_file_name}") + end + end + + def restore + case config["adapter"] + when /^mysql/ then + system("mysql #{mysql_args} #{config['database']} < #{db_file_name}") + when "postgresql" then + pg_env + system("psql #{config['database']} -f #{db_file_name}") + end + end + + protected + + def db_file_name + File.join(db_dir, 'database.sql') + end + + def mysql_args + args = { + 'host' => '--host', + 'port' => '--port', + 'socket' => '--socket', + 'username' => '--user', + 'encoding' => '--default-character-set', + 'password' => '--password' + } + args.map { |opt, arg| "#{arg}='#{config[opt]}'" if config[opt] }.compact.join(' ') + end + + def pg_env + ENV['PGUSER'] = config["username"] if config["username"] + ENV['PGHOST'] = config["host"] if config["host"] + ENV['PGPORT'] = config["port"].to_s if config["port"] + ENV['PGPASSWORD'] = config["password"].to_s if config["password"] + end + end +end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb new file mode 100644 index 00000000000..258a0fb2589 --- /dev/null +++ b/lib/backup/manager.rb @@ -0,0 +1,106 @@ +module Backup + class Manager + def pack + # saving additional informations + s = {} + s[:db_version] = "#{ActiveRecord::Migrator.current_version}" + s[:backup_created_at] = Time.now + s[:gitlab_version] = %x{git rev-parse HEAD}.gsub(/\n/,"") + s[:tar_version] = %x{tar --version | head -1}.gsub(/\n/,"") + + Dir.chdir(Gitlab.config.backup.path) + + File.open("#{Gitlab.config.backup.path}/backup_information.yml", "w+") do |file| + file << s.to_yaml.gsub(/^---\n/,'') + end + + # create archive + print "Creating backup archive: #{s[:backup_created_at].to_i}_gitlab_backup.tar ... " + if Kernel.system("tar -cf #{s[:backup_created_at].to_i}_gitlab_backup.tar repositories/ db/ uploads/ backup_information.yml") + puts "done".green + else + puts "failed".red + end + end + + def cleanup + print "Deleting tmp directories ... " + if Kernel.system("rm -rf repositories/ db/ uploads/ backup_information.yml") + puts "done".green + else + puts "failed".red + end + end + + def remove_old + # delete backups + print "Deleting old backups ... " + keep_time = Gitlab.config.backup.keep_time.to_i + path = Gitlab.config.backup.path + + if keep_time > 0 + removed = 0 + file_list = Dir.glob(Rails.root.join(path, "*_gitlab_backup.tar")) + file_list.map! { |f| $1.to_i if f =~ /(\d+)_gitlab_backup.tar/ } + file_list.sort.each do |timestamp| + if Time.at(timestamp) < (Time.now - keep_time) + if system("rm #{timestamp}_gitlab_backup.tar") + removed += 1 + end + end + end + puts "done. (#{removed} removed)".green + else + puts "skipping".yellow + end + end + + def unpack + Dir.chdir(Gitlab.config.backup.path) + + # 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" + exit 1 + end + + tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_backup.tar") + + unless File.exists?(tar_file) + puts "The specified backup doesn't exist!" + exit 1 + end + + print "Unpacking backup ... " + unless Kernel.system("tar -xf #{tar_file}") + puts "failed".red + exit 1 + else + puts "done".green + end + + settings = YAML.load_file("backup_information.yml") + ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0 + + # backups directory is not always sub of Rails root and able to execute the git rev-parse below + begin + Dir.chdir(Rails.root) + + # restoring mismatching backups can lead to unexpected problems + if settings[:gitlab_version] != %x{git rev-parse HEAD}.gsub(/\n/, "") + puts "GitLab version mismatch:".red + puts " Your current HEAD differs from the HEAD in the backup!".red + puts " Please switch to the following revision and try again:".red + puts " revision: #{settings[:gitlab_version]}".red + exit 1 + end + ensure + # chdir back to original intended dir + Dir.chdir(Gitlab.config.backup.path) + end + end + end +end diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb new file mode 100644 index 00000000000..c5e3d049fd7 --- /dev/null +++ b/lib/backup/repository.rb @@ -0,0 +1,105 @@ +require 'yaml' + +module Backup + class Repository + attr_reader :repos_path + + def dump + prepare + + Project.find_each(batch_size: 1000) do |project| + print " * #{project.path_with_namespace} ... " + + if project.empty_repo? + puts "[SKIPPED]".cyan + next + end + + # Create namespace dir if missing + FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace + + if system("cd #{path_to_repo(project)} > /dev/null 2>&1 && git bundle create #{path_to_bundle(project)} --all > /dev/null 2>&1") + puts "[DONE]".green + else + puts "[FAILED]".red + end + + wiki = GollumWiki.new(project) + + if File.exists?(path_to_repo(wiki)) + print " * #{wiki.path_with_namespace} ... " + if system("cd #{path_to_repo(wiki)} > /dev/null 2>&1 && git bundle create #{path_to_bundle(wiki)} --all > /dev/null 2>&1") + puts " [DONE]".green + else + puts " [FAILED]".red + end + end + end + end + + def restore + if File.exists?(repos_path) + # Move repos dir to 'repositories.old' dir + bk_repos_path = File.join(repos_path, '..', 'repositories.old.' + Time.now.to_i.to_s) + FileUtils.mv(repos_path, bk_repos_path) + end + + FileUtils.mkdir_p(repos_path) + + Project.find_each(batch_size: 1000) do |project| + print "#{project.path_with_namespace} ... " + + project.namespace.ensure_dir_exist if project.namespace + + if system("git clone --bare #{path_to_bundle(project)} #{path_to_repo(project)} > /dev/null 2>&1") + puts "[DONE]".green + else + puts "[FAILED]".red + end + + wiki = GollumWiki.new(project) + + if File.exists?(path_to_bundle(wiki)) + print " * #{wiki.path_with_namespace} ... " + if system("git clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)} > /dev/null 2>&1") + puts " [DONE]".green + else + puts " [FAILED]".red + end + end + end + + print 'Put GitLab hooks in repositories dirs'.yellow + gitlab_shell_user_home = File.expand_path("~#{Gitlab.config.gitlab_shell.ssh_user}") + if system("#{gitlab_shell_user_home}/gitlab-shell/support/rewrite-hooks.sh #{Gitlab.config.gitlab_shell.repos_path}") + puts " [DONE]".green + else + puts " [FAILED]".red + end + + end + + protected + + def path_to_repo(project) + File.join(repos_path, project.path_with_namespace + '.git') + end + + def path_to_bundle(project) + File.join(backup_repos_path, project.path_with_namespace + ".bundle") + end + + def repos_path + Gitlab.config.gitlab_shell.repos_path + end + + def backup_repos_path + File.join(Gitlab.config.backup.path, "repositories") + end + + def prepare + FileUtils.rm_rf(backup_repos_path) + FileUtils.mkdir_p(backup_repos_path) + end + end +end diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb new file mode 100644 index 00000000000..462d3f1e274 --- /dev/null +++ b/lib/backup/uploads.rb @@ -0,0 +1,29 @@ +module Backup + class Uploads + attr_reader :app_uploads_dir, :backup_uploads_dir, :backup_dir + + def initialize + @app_uploads_dir = Rails.root.join('public', 'uploads') + @backup_dir = Gitlab.config.backup.path + @backup_uploads_dir = File.join(Gitlab.config.backup.path, 'uploads') + end + + # Copy uploads from public/uploads to backup/uploads + def dump + FileUtils.mkdir_p(backup_uploads_dir) + FileUtils.cp_r(app_uploads_dir, backup_dir) + end + + def restore + backup_existing_uploads_dir + + FileUtils.cp_r(backup_uploads_dir, app_uploads_dir) + end + + def backup_existing_uploads_dir + if File.exists?(app_uploads_dir) + FileUtils.mv(app_uploads_dir, Rails.root.join('public', "uploads.#{Time.now.to_i}")) + end + end + end +end diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index fb595e18b24..53bc079296a 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -8,7 +8,7 @@ module ExtractsPath included do if respond_to?(:before_filter) - before_filter :assign_ref_vars, only: [:show] + before_filter :assign_ref_vars end end @@ -33,7 +33,7 @@ module ExtractsPath # extract_ref("v2.0.0/README.md") # # => ['v2.0.0', 'README.md'] # - # extract_ref('/gitlab/vagrant/tree/master/app/models/project.rb') + # extract_ref('master/app/models/project.rb') # # => ['master', 'app/models/project.rb'] # # extract_ref('issues/1234/app/models/project.rb') @@ -45,22 +45,12 @@ module ExtractsPath # # Returns an Array where the first value is the tree-ish and the second is the # path - def extract_ref(input) + def extract_ref(id) pair = ['', ''] return pair unless @project - # Remove relative_url_root from path - input.gsub!(/^#{Gitlab.config.gitlab.relative_url_root}/, "") - # Remove project, actions and all other staff from path - input.gsub!(/^\/#{Regexp.escape(@project.path_with_namespace)}/, "") - input.gsub!(/^\/(tree|commits|blame|blob|refs|graph)\//, "") # remove actions - input.gsub!(/\?.*$/, "") # remove stamps suffix - input.gsub!(/.atom$/, "") # remove rss feed - input.gsub!(/.json$/, "") # remove json suffix - input.gsub!(/\/edit$/, "") # remove edit route part - - if input.match(/^([[:alnum:]]{40})(.+)/) + if id.match(/^([[:alnum:]]{40})(.+)/) # If the ref appears to be a SHA, we're done, just split the string pair = $~.captures else @@ -68,7 +58,6 @@ module ExtractsPath # branches and tags # Append a trailing slash if we only get a ref and no file path - id = input id += '/' unless id.ends_with?('/') valid_refs = @project.repository.ref_names @@ -96,8 +85,8 @@ module ExtractsPath # - @id - A string representing the joined ref and path # - @ref - A string representing the ref (e.g., the branch, tag, or commit SHA) # - @path - A string representing the filesystem path - # - @commit - A CommitDecorator representing the commit from the given ref - # - @tree - A TreeDecorator representing the tree at the given ref/path + # - @commit - A Commit representing the commit from the given ref + # - @tree - A Tree representing the tree at the given ref/path # # If the :id parameter appears to be requesting a specific response format, # that will be handled as well. @@ -105,28 +94,33 @@ module ExtractsPath # Automatically renders `not_found!` if a valid tree path could not be # resolved (e.g., when a user inserts an invalid path or ref). def assign_ref_vars - # Handle formats embedded in the id - if params[:id].ends_with?('.atom') - params[:id].gsub!(/\.atom$/, '') - request.format = :atom + # assign allowed options + allowed_options = ["filter_ref", "extended_sha1"] + @options = params.select {|key, value| allowed_options.include?(key) && !value.blank? } + @options = HashWithIndifferentAccess.new(@options) + + @id = get_id + @ref, @path = extract_ref(@id) + @repo = @project.repository + if @options[:extended_sha1].blank? + @commit = @repo.commit(@ref) + else + @commit = @repo.commit(@options[:extended_sha1]) end + @tree = Tree.new(@repo, @commit.id, @ref, @path) + @hex_path = Digest::SHA1.hexdigest(@path) + @logs_path = logs_file_project_ref_path(@project, @ref, @path) - path = CGI::unescape(request.fullpath.dup) - - @ref, @path = extract_ref(path) - - @id = File.join(@ref, @path) - - # It is used "@project.repository.commits(@ref, @path, 1, 0)", - # because "@project.repository.commit(@ref)" returns wrong commit when @ref is tag name. - commits = @project.repository.commits(@ref, @path, 1, 0) - @commit = CommitDecorator.decorate(commits.first) + raise InvalidPathError unless @tree.exists? + rescue RuntimeError, NoMethodError, InvalidPathError + not_found! + end - @tree = Tree.new(@commit.tree, @ref, @path) - @tree = TreeDecorator.new(@tree) + private - raise InvalidPathError if @tree.invalid? - rescue NoMethodError, InvalidPathError - not_found! + def get_id + id = params[:id] || params[:ref] + id += "/" + params[:path] unless params[:path].blank? + id end end diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb new file mode 100644 index 00000000000..87f9cfab608 --- /dev/null +++ b/lib/gitlab/access.rb @@ -0,0 +1,52 @@ +# Gitlab::Access module +# +# Define allowed roles that can be used +# in GitLab code to determine authorization level +# +module Gitlab + module Access + GUEST = 10 + REPORTER = 20 + DEVELOPER = 30 + MASTER = 40 + OWNER = 50 + + class << self + def values + options.values + end + + def options + { + "Guest" => GUEST, + "Reporter" => REPORTER, + "Developer" => DEVELOPER, + "Master" => MASTER, + } + end + + def options_with_owner + options.merge( + "Owner" => OWNER + ) + end + + def sym_options + { + guest: GUEST, + reporter: REPORTER, + developer: DEVELOPER, + master: MASTER, + } + end + end + + def human_access + Gitlab::Access.options_with_owner.key(access_field) + end + + def owner? + access_field == OWNER + end + end +end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index d0e792befbb..0f196297477 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -1,72 +1,24 @@ module Gitlab class Auth - def find_for_ldap_auth(auth, signed_in_resource = nil) - uid = auth.info.uid - provider = auth.provider - email = auth.info.email.downcase unless auth.info.email.nil? - raise OmniAuth::Error, "LDAP accounts must provide an uid and email address" if uid.nil? or email.nil? + def find(login, password) + user = User.find_by_email(login) || User.find_by_username(login) - if @user = User.find_by_extern_uid_and_provider(uid, provider) - @user - elsif @user = User.find_by_email(email) - log.info "Updating legacy LDAP user #{email} with extern_uid => #{uid}" - @user.update_attributes(:extern_uid => uid, :provider => provider) - @user - else - create_from_omniauth(auth, true) - end - end + if user.nil? || user.ldap_user? + # Second chance - try LDAP authentication + return nil unless ldap_conf.enabled - def create_from_omniauth(auth, ldap = false) - provider = auth.provider - uid = auth.info.uid || auth.uid - uid = uid.to_s.force_encoding("utf-8") - name = auth.info.name.to_s.force_encoding("utf-8") - email = auth.info.email.to_s.downcase unless auth.info.email.nil? - - ldap_prefix = ldap ? '(LDAP) ' : '' - raise OmniAuth::Error, "#{ldap_prefix}#{provider} does not provide an email"\ - " address" if auth.info.email.blank? - - log.info "#{ldap_prefix}Creating user from #{provider} login"\ - " {uid => #{uid}, name => #{name}, email => #{email}}" - password = Devise.friendly_token[0, 8].downcase - @user = User.new({ - extern_uid: uid, - provider: provider, - name: name, - username: email.match(/^[^@]*/)[0], - email: email, - password: password, - password_confirmation: password, - projects_limit: Gitlab.config.gitlab.default_projects_limit, - }, as: :admin) - if Gitlab.config.omniauth['block_auto_created_users'] && !ldap - @user.blocked = true - end - @user.save! - @user - end - - def find_or_new_for_omniauth(auth) - provider, uid = auth.provider, auth.uid - email = auth.info.email.downcase unless auth.info.email.nil? - - if @user = User.find_by_provider_and_extern_uid(provider, uid) - @user - elsif @user = User.find_by_email(email) - @user.update_attributes(:extern_uid => uid, :provider => provider) - @user + Gitlab::LDAP::User.authenticate(login, password) else - if Gitlab.config.omniauth['allow_single_sign_on'] - @user = create_from_omniauth(auth) - @user - end + user if user.valid_password?(password) end end def log Gitlab::AppLogger end + + def ldap_conf + @ldap_conf ||= Gitlab.config.ldap + end end end diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index abbee6132d3..c522e0a413b 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -1,8 +1,11 @@ require_relative 'shell_env' +require_relative 'grack_helpers' module Grack class Auth < Rack::Auth::Basic - attr_accessor :user, :project + include Helpers + + attr_accessor :user, :project, :ref, :env def call(env) @env = env @@ -10,52 +13,73 @@ module Grack @auth = Request.new(env) # Need this patch due to the rails mount - @env['PATH_INFO'] = @request.path - @env['SCRIPT_NAME'] = "" - return render_not_found unless project - return unauthorized unless project.public || @auth.provided? - return bad_request if @auth.provided? && !@auth.basic? - - if valid? - if @auth.provided? - @env['REMOTE_USER'] = @auth.username - end - return @app.call(env) + # Need this if under RELATIVE_URL_ROOT + unless Gitlab.config.gitlab.relative_url_root.empty? + # If website is mounted using relative_url_root need to remove it first + @env['PATH_INFO'] = @request.path.sub(Gitlab.config.gitlab.relative_url_root,'') else - unauthorized + @env['PATH_INFO'] = @request.path end + + @env['SCRIPT_NAME'] = "" + + auth! end - def valid? + private + + def auth! + return render_not_found unless project + if @auth.provided? + return bad_request unless @auth.basic? + # Authentication with username and password login, password = @auth.credentials - self.user = User.find_by_email(login) || User.find_by_username(login) - return false unless user.try(:valid_password?, password) - Gitlab::ShellEnv.set_env(user) + @user = authenticate_user(login, password) + + if @user + Gitlab::ShellEnv.set_env(@user) + @env['REMOTE_USER'] = @auth.username + else + return unauthorized + end + + else + return unauthorized unless project.public end + if authorized_git_request? + @app.call(env) + else + unauthorized + end + end + + def authorized_git_request? # Git upload and receive if @request.get? - validate_get_request + authorize_request(@request.params['service']) elsif @request.post? - validate_post_request + authorize_request(File.basename(@request.path)) else false end end - def validate_get_request - project.public || can?(user, :download_code, project) + def authenticate_user(login, password) + auth = Gitlab::Auth.new + auth.find(login, password) end - def validate_post_request - if @request.path_info.end_with?('git-upload-pack') + def authorize_request(service) + case service + when 'git-upload-pack' project.public || can?(user, :download_code, project) - elsif @request.path_info.end_with?('git-receive-pack') - action = if project.protected_branch?(current_ref) + when'git-receive-pack' + action = if project.protected_branch?(ref) :push_code_to_protected_branches else :push_code @@ -67,45 +91,24 @@ module Grack end end - def can?(object, action, subject) - abilities.allowed?(object, action, subject) - end - - def current_ref - if @env["HTTP_CONTENT_ENCODING"] =~ /gzip/ - input = Zlib::GzipReader.new(@request.body).read - else - input = @request.body.read - end - # Need to reset seek point - @request.body.rewind - /refs\/heads\/([\w\.-]+)/.match(input).to_a.last - end - def project - unless instance_variable_defined? :@project - # Find project by PATH_INFO from env - if m = /^\/([\w\.\/-]+)\.git/.match(@request.path_info).to_a - @project = Project.find_with_namespace(m.last) - end - end - return @project + @project ||= project_by_path(@request.path_info) end - PLAIN_TYPE = {"Content-Type" => "text/plain"} - - def render_not_found - [404, PLAIN_TYPE, ["Not Found"]] + def ref + @ref ||= parse_ref end - protected + def parse_ref + input = if @env["HTTP_CONTENT_ENCODING"] =~ /gzip/ + Zlib::GzipReader.new(@request.body).read + else + @request.body.read + end - def abilities - @abilities ||= begin - abilities = Six.new - abilities << Ability - abilities - end + # Need to reset seek point + @request.body.rewind + /refs\/heads\/([\/\w\.-]+)/n.match(input.force_encoding('ascii-8bit')).to_a.last end - end# Auth -end# Grack + end +end diff --git a/lib/gitlab/backend/grack_helpers.rb b/lib/gitlab/backend/grack_helpers.rb new file mode 100644 index 00000000000..5ac9e9f325b --- /dev/null +++ b/lib/gitlab/backend/grack_helpers.rb @@ -0,0 +1,28 @@ +module Grack + module Helpers + def project_by_path(path) + if m = /^\/([\w\.\/-]+)\.git/.match(path).to_a + path_with_namespace = m.last + path_with_namespace.gsub!(/\.wiki$/, '') + + Project.find_with_namespace(path_with_namespace) + end + end + + def render_not_found + [404, {"Content-Type" => "text/plain"}, ["Not Found"]] + end + + def can?(object, action, subject) + abilities.allowed?(object, action, subject) + end + + def abilities + @abilities ||= begin + abilities = Six.new + abilities << Ability + abilities + end + end + end +end diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index b7b92e86a87..c819ce56ac9 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -10,7 +10,7 @@ module Gitlab # add_repository("gitlab/gitlab-ci") # def add_repository(name) - system("/home/git/gitlab-shell/bin/gitlab-projects add-project #{name}.git") + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "add-project", "#{name}.git" end # Import repository @@ -21,7 +21,43 @@ module Gitlab # import_repository("gitlab/gitlab-ci", "https://github.com/randx/six.git") # def import_repository(name, url) - system("/home/git/gitlab-shell/bin/gitlab-projects import-project #{name}.git #{url}") + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "import-project", "#{name}.git", url + end + + # Move repository + # + # path - project path with namespace + # new_path - new project path with namespace + # + # Ex. + # mv_repository("gitlab/gitlab-ci", "randx/gitlab-ci-new.git") + # + def mv_repository(path, new_path) + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "mv-project", "#{path}.git", "#{new_path}.git" + end + + # Update HEAD for repository + # + # path - project path with namespace + # branch - repository branch name + # + # Ex. + # update_repository_head("gitlab/gitlab-ci", "3-1-stable") + # + def update_repository_head(path, branch) + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "update-head", "#{path}.git", branch + end + + # Fork repository to new namespace + # + # path - project path with namespace + # fork_namespace - namespace for forked project + # + # Ex. + # fork_repository("gitlab/gitlab-ci", "randx") + # + def fork_repository(path, fork_namespace) + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "fork-project", "#{path}.git", fork_namespace end # Remove repository from file system @@ -32,7 +68,57 @@ module Gitlab # remove_repository("gitlab/gitlab-ci") # def remove_repository(name) - system("/home/git/gitlab-shell/bin/gitlab-projects rm-project #{name}.git") + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "rm-project", "#{name}.git" + end + + # Add repository branch from passed ref + # + # path - project path with namespace + # branch_name - new branch name + # ref - HEAD for new branch + # + # Ex. + # add_branch("gitlab/gitlab-ci", "4-0-stable", "master") + # + def add_branch(path, branch_name, ref) + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "create-branch", "#{path}.git", branch_name, ref + end + + # Remove repository branch + # + # path - project path with namespace + # branch_name - branch name to remove + # + # Ex. + # rm_branch("gitlab/gitlab-ci", "4-0-stable") + # + def rm_branch(path, branch_name) + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "rm-branch", "#{path}.git", branch_name + end + + # Add repository tag from passed ref + # + # path - project path with namespace + # tag_name - new tag name + # ref - HEAD for new tag + # + # Ex. + # add_tag("gitlab/gitlab-ci", "v4.0", "master") + # + def add_tag(path, tag_name, ref) + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "create-tag", "#{path}.git", tag_name, ref + end + + # Remove repository tag + # + # path - project path with namespace + # tag_name - tag name to remove + # + # Ex. + # rm_tag("gitlab/gitlab-ci", "v4.0") + # + def rm_tag(path, tag_name) + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-projects", "rm-tag", "#{path}.git", tag_name end # Add new key to gitlab-shell @@ -41,7 +127,7 @@ module Gitlab # add_key("key-42", "sha-rsa ...") # def add_key(key_id, key_content) - system("/home/git/gitlab-shell/bin/gitlab-keys add-key #{key_id} \"#{key_content}\"") + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-keys", "add-key", key_id, key_content end # Remove ssh key from gitlab shell @@ -50,11 +136,84 @@ module Gitlab # remove_key("key-342", "sha-rsa ...") # def remove_key(key_id, key_content) - system("/home/git/gitlab-shell/bin/gitlab-keys rm-key #{key_id} \"#{key_content}\"") + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-keys", "rm-key", key_id, key_content + end + + # Remove all ssh keys from gitlab shell + # + # Ex. + # remove_all_keys + # + def remove_all_keys + system "#{gitlab_shell_user_home}/gitlab-shell/bin/gitlab-keys", "clear" + end + + # Add empty directory for storing repositories + # + # Ex. + # add_namespace("gitlab") + # + def add_namespace(name) + FileUtils.mkdir(full_path(name), mode: 0770) unless exists?(name) + end + + # Remove directory from repositories storage + # Every repository inside this directory will be removed too + # + # Ex. + # rm_namespace("gitlab") + # + def rm_namespace(name) + FileUtils.rm_r(full_path(name), force: true) + end + + # Move namespace directory inside repositories storage + # + # Ex. + # mv_namespace("gitlab", "gitlabhq") + # + def mv_namespace(old_name, new_name) + return false if exists?(new_name) || !exists?(old_name) + + FileUtils.mv(full_path(old_name), full_path(new_name)) + end + + # Remove GitLab Satellites for provided path (namespace or repo dir) + # + # Ex. + # rm_satellites("gitlab") + # + # rm_satellites("gitlab/gitlab-ci.git") + # + def rm_satellites(path) + raise ArgumentError.new("Path can't be blank") if path.blank? + + satellites_path = File.join(Gitlab.config.satellites.path, path) + FileUtils.rm_r(satellites_path, force: true) end def url_to_repo path Gitlab.config.gitlab_shell.ssh_path_prefix + "#{path}.git" end + + protected + + def gitlab_shell_user_home + File.expand_path("~#{Gitlab.config.gitlab_shell.ssh_user}") + end + + def repos_path + Gitlab.config.gitlab_shell.repos_path + end + + def full_path(dir_name) + raise ArgumentError.new("Directory name can't be blank") if dir_name.blank? + + File.join(repos_path, dir_name) + end + + def exists?(dir_name) + File.exists?(full_path(dir_name)) + end end end diff --git a/lib/gitlab/backend/shell_adapter.rb b/lib/gitlab/backend/shell_adapter.rb new file mode 100644 index 00000000000..f247f4593d7 --- /dev/null +++ b/lib/gitlab/backend/shell_adapter.rb @@ -0,0 +1,12 @@ +# == GitLab Shell mixin +# +# Provide a shortcut to Gitlab::Shell instance by gitlab_shell +# +module Gitlab + module ShellAdapter + def gitlab_shell + Gitlab::Shell.new + end + end +end + diff --git a/lib/gitlab/backend/shell_env.rb b/lib/gitlab/backend/shell_env.rb index 15721875093..044afb27f3f 100644 --- a/lib/gitlab/backend/shell_env.rb +++ b/lib/gitlab/backend/shell_env.rb @@ -1,6 +1,6 @@ module Gitlab # This module provide 2 methods - # to set specific ENV variabled for GitLab Shell + # to set specific ENV variables for GitLab Shell module ShellEnv extend self diff --git a/lib/gitlab/blacklist.rb b/lib/gitlab/blacklist.rb new file mode 100644 index 00000000000..2f9091e07df --- /dev/null +++ b/lib/gitlab/blacklist.rb @@ -0,0 +1,9 @@ +module Gitlab + module Blacklist + extend self + + def path + %w(admin dashboard groups help profile projects search public assets u s teams merge_requests issues users snippets services repository hooks notes) + end + end +end diff --git a/lib/gitlab/diff_parser.rb b/lib/gitlab/diff_parser.rb new file mode 100644 index 00000000000..fb27280c4a4 --- /dev/null +++ b/lib/gitlab/diff_parser.rb @@ -0,0 +1,77 @@ +module Gitlab + class DiffParser + include Enumerable + + attr_reader :lines, :new_path + + def initialize(diff) + @lines = diff.diff.lines.to_a + @new_path = diff.new_path + end + + def each + line_old = 1 + line_new = 1 + type = nil + + lines_arr = ::Gitlab::InlineDiff.processing lines + lines_arr.each do |line| + raw_line = line.dup + + next if line.match(/^\-\-\- \/dev\/null/) + next if line.match(/^\+\+\+ \/dev\/null/) + next if line.match(/^\-\-\- a/) + next if line.match(/^\+\+\+ b/) + + full_line = html_escape(line.gsub(/\n/, '')) + full_line = ::Gitlab::InlineDiff.replace_markers full_line + + if line.match(/^@@ -/) + type = "match" + + line_old = line.match(/\-[0-9]*/)[0].to_i.abs rescue 0 + line_new = line.match(/\+[0-9]*/)[0].to_i.abs rescue 0 + + next if line_old == 1 && line_new == 1 #top of file + yield(full_line, type, nil, nil, nil) + next + else + type = identification_type(line) + line_code = generate_line_code(new_path, line_new, line_old) + yield(full_line, type, line_code, line_new, line_old, raw_line) + end + + + if line[0] == "+" + line_new += 1 + elsif line[0] == "-" + line_old += 1 + else + line_new += 1 + line_old += 1 + end + end + end + + private + + def identification_type(line) + if line[0] == "+" + "new" + elsif line[0] == "-" + "old" + else + nil + end + end + + def generate_line_code(path, line_new, line_old) + "#{Digest::SHA1.hexdigest(path)}_#{line_old}_#{line_new}" + end + + def html_escape str + replacements = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' } + str.gsub(/[&"'><]/, replacements) + end + end +end diff --git a/lib/gitlab/git_stats.rb b/lib/gitlab/git_stats.rb deleted file mode 100644 index 855bffb5dde..00000000000 --- a/lib/gitlab/git_stats.rb +++ /dev/null @@ -1,73 +0,0 @@ -module Gitlab - class GitStats - attr_accessor :repo, :ref - - def initialize repo, ref - @repo, @ref = repo, ref - end - - def authors - @authors ||= collect_authors - end - - def commits_count - @commits_count ||= repo.commit_count(ref) - end - - def files_count - args = [ref, '-r', '--name-only' ] - repo.git.run(nil, 'ls-tree', nil, {}, args).split("\n").count - end - - def authors_count - authors.size - end - - def graph - @graph ||= build_graph - end - - protected - - def collect_authors - shortlog = repo.git.shortlog({e: true, s: true }, ref) - - authors = [] - - lines = shortlog.split("\n") - - lines.each do |line| - data = line.split("\t") - commits = data.first - author = Grit::Actor.from_string(data.last) - - authors << OpenStruct.new( - name: author.name, - email: author.email, - commits: commits.to_i - ) - end - - authors.sort_by(&:commits).reverse - end - - def build_graph n = 4 - from, to = (Date.today - n.weeks), Date.today - args = ['--all', "--since=#{from.to_s(:date)}", '--format=%ad' ] - rev_list = repo.git.run(nil, 'rev-list', nil, {}, args).split("\n") - - commits_dates = rev_list.values_at(* rev_list.each_index.select {|i| i.odd?}) - commits_dates = commits_dates.map { |date_str| Time.parse(date_str).to_date.to_s(:date) } - - commits_per_day = from.upto(to).map do |day| - commits_dates.count(day.to_date.to_s(:date)) - end - - OpenStruct.new( - labels: from.upto(to).map { |day| day.stamp('Aug 23') }, - commits: commits_per_day, - weeks: n - ) - end - end -end diff --git a/lib/gitlab/graph/commit.rb b/lib/gitlab/graph/commit.rb deleted file mode 100644 index 13c8ebc9952..00000000000 --- a/lib/gitlab/graph/commit.rb +++ /dev/null @@ -1,52 +0,0 @@ -require "grit" - -module Gitlab - module Graph - class Commit - include ActionView::Helpers::TagHelper - - attr_accessor :time, :space, :refs, :parent_spaces - - def initialize(commit) - @_commit = commit - @time = -1 - @space = 0 - @parent_spaces = [] - end - - def method_missing(m, *args, &block) - @_commit.send(m, *args, &block) - end - - def to_graph_hash - h = {} - h[:parents] = self.parents.collect do |p| - [p.id,0,0] - end - h[:author] = { - name: author.name, - email: author.email - } - h[:time] = time - h[:space] = space - h[:parent_spaces] = parent_spaces - h[:refs] = refs.collect{|r|r.name}.join(" ") unless refs.nil? - h[:id] = sha - h[:date] = date - h[:message] = message - h - end - - def add_refs(ref_cache, repo) - if ref_cache.empty? - repo.refs.each do |ref| - ref_cache[ref.commit.id] ||= [] - ref_cache[ref.commit.id] << ref - end - end - @refs = ref_cache[@_commit.id] if ref_cache.include?(@_commit.id) - @refs ||= [] - end - end - end -end diff --git a/lib/gitlab/graph/json_builder.rb b/lib/gitlab/graph/json_builder.rb deleted file mode 100644 index cc971a245a7..00000000000 --- a/lib/gitlab/graph/json_builder.rb +++ /dev/null @@ -1,268 +0,0 @@ -require "grit" - -module Gitlab - module Graph - class JsonBuilder - attr_accessor :days, :commits, :ref_cache, :repo - - def self.max_count - @max_count ||= 650 - end - - def initialize project, ref, commit - @project = project - @ref = ref - @commit = commit - @repo = project.repo - @ref_cache = {} - - @commits = collect_commits - @days = index_commits - end - - def to_json(*args) - { - days: @days.compact.map { |d| [d.day, d.strftime("%b")] }, - commits: @commits.map(&:to_graph_hash) - }.to_json(*args) - end - - protected - - # Get commits from repository - # - def collect_commits - - @commits = Grit::Commit.find_all(repo, nil, {topo_order: true, max_count: self.class.max_count, skip: to_commit}).dup - - # Decorate with app/models/commit.rb - @commits.map! { |commit| ::Commit.new(commit) } - - # Decorate with lib/gitlab/graph/commit.rb - @commits.map! { |commit| Gitlab::Graph::Commit.new(commit) } - - # add refs to each commit - @commits.each { |commit| commit.add_refs(ref_cache, repo) } - - @commits - end - - # Method is adding time and space on the - # list of commits. As well as returns date list - # corelated with time set on commits. - # - # @param [Array<Graph::Commit>] commits to index - # - # @return [Array<TimeDate>] list of commit dates corelated with time on commits - def index_commits - days, times = [], [] - map = {} - - commits.reverse.each_with_index do |c,i| - c.time = i - days[i] = c.committed_date - map[c.id] = c - times[i] = c - end - - @_reserved = {} - days.each_index do |i| - @_reserved[i] = [] - end - - commits_sort_by_ref.each do |commit| - if map.include? commit.id then - place_chain(map[commit.id], map) - end - end - - # find parent spaces for not overlap lines - times.each do |c| - c.parent_spaces.concat(find_free_parent_spaces(c, map, times)) - end - - days - end - - # Skip count that the target commit is displayed in center. - def to_commit - commits = Grit::Commit.find_all(repo, nil, {topo_order: true}) - commit_index = commits.index do |c| - c.id == @commit.id - end - - if commit_index && (self.class.max_count / 2 < commit_index) then - # get max index that commit is displayed in the center. - commit_index - self.class.max_count / 2 - else - 0 - end - end - - def commits_sort_by_ref - commits.sort do |a,b| - if include_ref?(a) - -1 - elsif include_ref?(b) - 1 - else - b.committed_date <=> a.committed_date - end - end - end - - def include_ref?(commit) - heads = commit.refs.select do |ref| - ref.is_a?(Grit::Head) or ref.is_a?(Grit::Remote) or ref.is_a?(Grit::Tag) - end - - heads.map! do |head| - head.name - end - - heads.include?(@ref) - end - - def find_free_parent_spaces(commit, map, times) - spaces = [] - - commit.parents.each do |p| - if map.include?(p.id) then - parent = map[p.id] - - range = if commit.time < parent.time then - commit.time..parent.time - else - parent.time..commit.time - end - - space = if commit.space >= parent.space then - find_free_parent_space(range, parent.space, 1, commit.space, times) - else - find_free_parent_space(range, parent.space, -1, parent.space, times) - end - - mark_reserved(range, space) - spaces << space - end - end - - spaces - end - - def find_free_parent_space(range, space_base, space_step, space_default, times) - if is_overlap?(range, times, space_default) then - find_free_space(range, space_base, space_step) - else - space_default - end - end - - def is_overlap?(range, times, overlap_space) - range.each do |i| - if i != range.first && - i != range.last && - times[i].space == overlap_space then - - return true; - end - end - - false - end - - # Add space mark on commit and its parents - # - # @param [Graph::Commit] the commit object. - # @param [Hash<String,Graph::Commit>] map of commits - def place_chain(commit, map, parent_time = nil) - leaves = take_left_leaves(commit, map) - if leaves.empty? - return - end - # and mark it as reserved - min_time = leaves.last.time - max_space = 1 - parents = leaves.last.parents.collect - parents.each do |p| - if map.include? p.id - parent = map[p.id] - if parent.time < min_time - min_time = parent.time - end - if max_space < parent.space then - max_space = parent.space - end - end - end - if parent_time.nil? - max_time = leaves.first.time - else - max_time = parent_time - 1 - end - - time_range = leaves.last.time..leaves.first.time - space = find_free_space(time_range, max_space, 2) - leaves.each{|l| l.space = space} - - mark_reserved(min_time..max_time, space) - - # Visit branching chains - leaves.each do |l| - parents = l.parents.collect.select{|p| map.include? p.id and map[p.id].space.zero?} - for p in parents - place_chain(map[p.id], map, l.time) - end - end - end - - def mark_reserved(time_range, space) - for day in time_range - @_reserved[day].push(space) - end - end - - def find_free_space(time_range, space_base, space_step) - reserved = [] - for day in time_range - reserved += @_reserved[day] - end - reserved.uniq! - - space = space_base - while reserved.include?(space) do - space += space_step - if space <= 0 then - space_step *= -1 - space = space_base + space_step - end - end - - space - end - - # Takes most left subtree branch of commits - # which don't have space mark yet. - # - # @param [Graph::Commit] the commit object. - # @param [Hash<String,Graph::Commit>] map of commits - # - # @return [Array<Graph::Commit>] list of branch commits - def take_left_leaves(commit, map) - leaves = [] - leaves.push(commit) if commit.space.zero? - - while true - return leaves if commit.parents.count.zero? - return leaves unless map.include? commit.parents.first.id - - commit = map[commit.parents.first.id] - - return leaves unless commit.space.zero? - - leaves.push(commit) - end - end - end - end -end diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb new file mode 100644 index 00000000000..a1ff248a77f --- /dev/null +++ b/lib/gitlab/identifier.rb @@ -0,0 +1,23 @@ +# Detect user based on identifier like +# key-13 or user-36 or last commit +module Gitlab + module Identifier + def identify(identifier, project, newrev) + if identifier.blank? + # Local push from gitlab + email = project.repository.commit(newrev).author_email rescue nil + User.find_by_email(email) if email + + elsif identifier =~ /\Auser-\d+\Z/ + # git push over http + user_id = identifier.gsub("user-", "") + User.find_by_id(user_id) + + elsif identifier =~ /\Akey-\d+\Z/ + # git push over ssh + key_id = identifier.gsub("key-", "") + Key.find_by_id(key_id).try(:user) + end + end + end +end diff --git a/lib/gitlab/inline_diff.rb b/lib/gitlab/inline_diff.rb index 7a0a3214aa1..89c8e0680c3 100644 --- a/lib/gitlab/inline_diff.rb +++ b/lib/gitlab/inline_diff.rb @@ -4,7 +4,7 @@ module Gitlab START = "#!idiff-start!#" FINISH = "#!idiff-finish!#" - + def processing diff_arr indexes = _indexes_of_changed_lines diff_arr @@ -13,6 +13,9 @@ module Gitlab second_line = diff_arr[index+2] max_length = [first_line.size, second_line.size].max + # Skip inline diff if empty line was replaced with content + next if first_line == "-\n" + first_the_same_symbols = 0 (0..max_length + 1).each do |i| first_the_same_symbols = i - 1 @@ -20,9 +23,19 @@ module Gitlab break end end + first_token = first_line[0..first_the_same_symbols][1..-1] - diff_arr[index+1].sub!(first_token, first_token + START) - diff_arr[index+2].sub!(first_token, first_token + START) + start = first_token + START + + if first_token.empty? + # In case if we remove string of spaces in commit + diff_arr[index+1].sub!("-", "-" => "-#{START}") + diff_arr[index+2].sub!("+", "+" => "+#{START}") + else + diff_arr[index+1].sub!(first_token, first_token => start) + diff_arr[index+2].sub!(first_token, first_token => start) + end + last_the_same_symbols = 0 (1..max_length + 1).each do |i| last_the_same_symbols = -i @@ -60,8 +73,6 @@ module Gitlab line.gsub!(FINISH, "</span>") line end - end - end end diff --git a/lib/gitlab/issues_labels.rb b/lib/gitlab/issues_labels.rb new file mode 100644 index 00000000000..bc49d27b521 --- /dev/null +++ b/lib/gitlab/issues_labels.rb @@ -0,0 +1,28 @@ +module Gitlab + class IssuesLabels + class << self + def important_labels + %w(bug critical confirmed) + end + + def warning_labels + %w(documentation support) + end + + def neutral_labels + %w(discussion suggestion) + end + + def positive_labels + %w(feature enhancement) + end + + def generate(project) + labels = important_labels + warning_labels + neutral_labels + positive_labels + + project.issues_default_label_list = labels + project.save + end + end + end +end diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb new file mode 100644 index 00000000000..260bacfeeb0 --- /dev/null +++ b/lib/gitlab/ldap/user.rb @@ -0,0 +1,94 @@ +require 'gitlab/oauth/user' + +# LDAP extension for User model +# +# * Find or create user from omniauth.auth data +# * Links LDAP account with existing user +# * Auth LDAP user with login and password +# +module Gitlab + module LDAP + class User < Gitlab::OAuth::User + class << self + def find_or_create(auth) + @auth = auth + + if uid.blank? || email.blank? + raise_error("Account must provide an uid and email address") + end + + user = find(auth) + + unless user + # Look for user with same emails + # + # Possible cases: + # * When user already has account and need to link his LDAP account. + # * LDAP uid changed for user with same email and we need to update his uid + # + user = find_user(email) + + if user + user.update_attributes(extern_uid: uid, provider: provider) + log.info("(LDAP) Updating legacy LDAP user #{email} with extern_uid => #{uid}") + else + # Create a new user inside GitLab database + # based on LDAP credentials + # + # + user = create(auth) + end + end + + user + end + + def find_user(email) + user = model.find_by_email(email) + + # If no user found and allow_username_or_email_login is true + # we look for user by extracting part of his email + if !user && email && ldap_conf['allow_username_or_email_login'] + uname = email.partition('@').first + user = model.find_by_username(uname) + end + + user + end + + def authenticate(login, password) + # Check user against LDAP backend if user is not authenticated + # Only check with valid login and password to prevent anonymous bind results + return nil unless ldap_conf.enabled && login.present? && password.present? + + ldap = OmniAuth::LDAP::Adaptor.new(ldap_conf) + ldap_user = ldap.bind_as( + filter: Net::LDAP::Filter.eq(ldap.uid, login), + size: 1, + password: password + ) + + find_by_uid(ldap_user.dn) if ldap_user + end + + private + + def find_by_uid(uid) + model.where(provider: provider, extern_uid: uid).last + end + + def provider + 'ldap' + end + + def raise_error(message) + raise OmniAuth::Error, "(LDAP) " + message + end + + def ldap_conf + Gitlab.config.ldap + end + end + end + end +end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index e7d6e3e6bd9..dc9c0e0ab2c 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -7,6 +7,7 @@ module Gitlab # Supported reference formats are: # * @foo for team members # * #123 for issues + # * #JIRA-123 for Jira issues # * !123 for merge requests # * $123 for snippets # * 123456 for commits @@ -17,7 +18,7 @@ module Gitlab # Examples # # >> gfm("Hey @david, can you fix this?") - # => "Hey <a href="/gitlab/team_members/1">@david</a>, can you fix this?" + # => "Hey <a href="/u/david">@david</a>, can you fix this?" # # >> gfm("Commit 35d5f7c closes #1234") # => "Commit <a href="/gitlab/commits/35d5f7c">35d5f7c</a> closes <a href="/gitlab/issues/1234">#1234</a>" @@ -25,6 +26,8 @@ module Gitlab # >> gfm(":trollface:") # => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" /> module Markdown + include IssuesHelper + attr_reader :html_options # Public: Parse the provided text with GitLab-Flavored Markdown @@ -60,7 +63,7 @@ module Gitlab insert_piece($1) end - sanitize text.html_safe, attributes: ActionView::Base.sanitized_allowed_attributes + %w(id class) + sanitize text.html_safe, attributes: ActionView::Base.sanitized_allowed_attributes + %w(id class), tags: ActionView::Base.sanitized_allowed_tags + %w(table tr td th) end private @@ -95,10 +98,11 @@ module Gitlab (?<prefix>\W)? # Prefix ( # Reference @(?<user>[a-zA-Z][a-zA-Z0-9_\-\.]*) # User name - |\#(?<issue>\d+) # Issue ID + |\#(?<issue>([a-zA-Z]+-)?\d+) # Issue ID |!(?<merge_request>\d+) # MR ID |\$(?<snippet>\d+) # Snippet ID |(?<commit>[\h]{6,40}) # Commit ID + |(?<skip>gfm-extraction-[\h]{6,40}) # Skip gfm extractions. Otherwise will be parsed as commit ) (?<suffix>\W)? # Suffix }x.freeze @@ -111,13 +115,18 @@ module Gitlab prefix = $~[:prefix] suffix = $~[:suffix] type = TYPES.select{|t| !$~[t].nil?}.first - identifier = $~[type] - # Avoid HTML entities - if prefix && suffix && prefix[0] == '&' && suffix[-1] == ';' - match - elsif ref_link = reference_link(type, identifier) - "#{prefix}#{ref_link}#{suffix}" + if type + identifier = $~[type] + + # Avoid HTML entities + if prefix && suffix && prefix[0] == '&' && suffix[-1] == ';' + match + elsif ref_link = reference_link(type, identifier) + "#{prefix}#{ref_link}#{suffix}" + else + match + end else match end @@ -157,19 +166,22 @@ module Gitlab end def reference_user(identifier) - if member = @project.users_projects.joins(:user).where(users: { username: identifier }).first - link_to("@#{identifier}", project_team_member_url(@project, member), html_options.merge(class: "gfm gfm-team_member #{html_options[:class]}")) if member + if member = @project.team_members.find { |user| user.username == identifier } + link_to("@#{identifier}", user_url(identifier), html_options.merge(class: "gfm gfm-team_member #{html_options[:class]}")) if member end end def reference_issue(identifier) - if issue = @project.issues.where(id: identifier).first - link_to("##{identifier}", project_issue_url(@project, issue), html_options.merge(title: "Issue: #{issue.title}", class: "gfm gfm-issue #{html_options[:class]}")) + if @project.issue_exists? identifier + url = url_for_issue(identifier) + title = title_for_issue(identifier) + + link_to("##{identifier}", url, html_options.merge(title: "Issue: #{title}", class: "gfm gfm-issue #{html_options[:class]}")) end end def reference_merge_request(identifier) - if merge_request = @project.merge_requests.where(id: identifier).first + if merge_request = @project.merge_requests.where(iid: identifier).first link_to("!#{identifier}", project_merge_request_url(@project, merge_request), html_options.merge(title: "Merge Request: #{merge_request.title}", class: "gfm gfm-merge_request #{html_options[:class]}")) end end @@ -182,7 +194,7 @@ module Gitlab def reference_commit(identifier) if @project.valid_repo? && commit = @project.repository.commit(identifier) - link_to(identifier, project_commit_url(@project, commit), html_options.merge(title: CommitDecorator.new(commit).link_title, class: "gfm gfm-commit #{html_options[:class]}")) + link_to(identifier, project_commit_url(@project, commit), html_options.merge(title: commit.link_title, class: "gfm gfm-commit #{html_options[:class]}")) end end end diff --git a/lib/gitlab/oauth/user.rb b/lib/gitlab/oauth/user.rb new file mode 100644 index 00000000000..1b32b99f4ba --- /dev/null +++ b/lib/gitlab/oauth/user.rb @@ -0,0 +1,85 @@ +# OAuth extension for User model +# +# * Find GitLab user based on omniauth uid and provider +# * Create new user from omniauth data +# +module Gitlab + module OAuth + class User + class << self + attr_reader :auth + + def find(auth) + @auth = auth + find_by_uid_and_provider + end + + def create(auth) + @auth = auth + password = Devise.friendly_token[0, 8].downcase + opts = { + extern_uid: uid, + provider: provider, + name: name, + username: username, + email: email, + password: password, + password_confirmation: password, + } + + user = model.build_user(opts, as: :admin) + user.save! + log.info "(OAuth) Creating user #{email} from login with extern_uid => #{uid}" + + if Gitlab.config.omniauth['block_auto_created_users'] && !ldap? + user.block + end + + user + end + + private + + def find_by_uid_and_provider + model.where(provider: provider, extern_uid: uid).last + end + + def uid + auth.info.uid || auth.uid + end + + def email + auth.info.email.downcase unless auth.info.email.nil? + end + + def name + auth.info.name.to_s.force_encoding("utf-8") + end + + def username + email.match(/^[^@]*/)[0] + end + + def provider + auth.provider + end + + def log + Gitlab::AppLogger + end + + def model + ::User + end + + def raise_error(message) + raise OmniAuth::Error, "(OAuth) " + message + end + + def ldap? + provider == 'ldap' + end + end + end + end +end diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb index f2cfd8073e3..2f30fde2078 100644 --- a/lib/gitlab/popen.rb +++ b/lib/gitlab/popen.rb @@ -2,7 +2,7 @@ module Gitlab module Popen def popen(cmd, path) vars = { "PWD" => path } - options = { :chdir => path } + options = { chdir: path } @cmd_output = "" @cmd_status = 0 diff --git a/lib/gitlab/project_mover.rb b/lib/gitlab/project_mover.rb deleted file mode 100644 index e21f45c6564..00000000000 --- a/lib/gitlab/project_mover.rb +++ /dev/null @@ -1,45 +0,0 @@ -# ProjectMover class -# -# Used for moving project repositories from one subdir to another -module Gitlab - class ProjectMover - class ProjectMoveError < StandardError; end - - attr_reader :project, :old_dir, :new_dir - - def initialize(project, old_dir, new_dir) - @project = project - @old_dir = old_dir - @new_dir = new_dir - end - - def execute - # Create new dir if missing - new_dir_path = File.join(Gitlab.config.gitlab_shell.repos_path, new_dir) - FileUtils.mkdir( new_dir_path, mode: 0770 ) unless File.exists?(new_dir_path) - - old_path = File.join(Gitlab.config.gitlab_shell.repos_path, old_dir, "#{project.path}.git") - new_path = File.join(new_dir_path, "#{project.path}.git") - - if File.exists? new_path - raise ProjectMoveError.new("Destination #{new_path} already exists") - end - - begin - FileUtils.mv( old_path, new_path ) - log_info "Project #{project.name} was moved from #{old_path} to #{new_path}" - true - rescue Exception => e - message = "Project #{project.name} cannot be moved from #{old_path} to #{new_path}" - log_info "Error! #{message} (#{e.message})" - raise ProjectMoveError.new(message) - end - end - - protected - - def log_info message - Gitlab::AppLogger.info message - end - end -end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb new file mode 100644 index 00000000000..94b01e808d9 --- /dev/null +++ b/lib/gitlab/reference_extractor.rb @@ -0,0 +1,59 @@ +module Gitlab + # Extract possible GFM references from an arbitrary String for further processing. + class ReferenceExtractor + attr_accessor :users, :issues, :merge_requests, :snippets, :commits + + include Markdown + + def initialize + @users, @issues, @merge_requests, @snippets, @commits = [], [], [], [], [] + end + + def analyze string + parse_references(string.dup) + end + + # Given a valid project, resolve the extracted identifiers of the requested type to + # model objects. + + def users_for project + users.map do |identifier| + project.users.where(username: identifier).first + end.reject(&:nil?) + end + + def issues_for project + issues.map do |identifier| + project.issues.where(iid: identifier).first + end.reject(&:nil?) + end + + def merge_requests_for project + merge_requests.map do |identifier| + project.merge_requests.where(iid: identifier).first + end.reject(&:nil?) + end + + def snippets_for project + snippets.map do |identifier| + project.snippets.where(id: identifier).first + end.reject(&:nil?) + end + + def commits_for project + repo = project.repository + return [] if repo.nil? + + commits.map do |identifier| + repo.commit(identifier) + end.reject(&:nil?) + end + + private + + def reference_link type, identifier + # Append identifier to the appropriate collection. + send("#{type}s") << identifier + end + end +end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 483042205ea..b4be46d3b42 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -7,7 +7,11 @@ module Gitlab end def project_name_regex - /\A[a-zA-Z][a-zA-Z0-9_\-\. ]*\z/ + /\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z/ + end + + def name_regex + /\A[a-zA-Z0-9_\-\. ]*\z/ end def path_regex @@ -17,7 +21,7 @@ module Gitlab protected def default_regex - /\A[a-zA-Z][a-zA-Z0-9_\-\.]*\z/ + /\A[a-zA-Z0-9][a-zA-Z0-9_\-\.]*\z/ end end end diff --git a/lib/gitlab/satellite/action.rb b/lib/gitlab/satellite/action.rb index 63303ca3de1..5ea6f956765 100644 --- a/lib/gitlab/satellite/action.rb +++ b/lib/gitlab/satellite/action.rb @@ -25,25 +25,31 @@ module Gitlab end end rescue Errno::ENOMEM => ex - Gitlab::GitLogger.error(ex.message) - return false + return handle_exception(ex) rescue Grit::Git::GitTimeout => ex - Gitlab::GitLogger.error(ex.message) - return false + return handle_exception(ex) ensure Gitlab::ShellEnv.reset_env end - # * Clears the satellite - # * Updates the satellite from Gitolite + # * Recreates the satellite # * Sets up Git variables for the user # # Note: use this within #in_locked_and_timed_satellite def prepare_satellite!(repo) project.satellite.clear_and_update! - repo.git.config({}, "user.name", user.name) - repo.git.config({}, "user.email", user.email) + repo.config['user.name'] = user.name + repo.config['user.email'] = user.email + end + + def default_options(options = {}) + {raise: true, timeout: true}.merge(options) + end + + def handle_exception(exception) + Gitlab::GitLogger.error(exception.message) + false end end end diff --git a/lib/gitlab/satellite/edit_file_action.rb b/lib/gitlab/satellite/edit_file_action.rb index e9053f904c0..d793d0ba8dc 100644 --- a/lib/gitlab/satellite/edit_file_action.rb +++ b/lib/gitlab/satellite/edit_file_action.rb @@ -13,7 +13,7 @@ module Gitlab # Updates the files content and creates a new commit for it # # Returns false if the ref has been updated while editing the file - # Returns false if commiting the change fails + # Returns false if committing the change fails # Returns false if pushing from the satellite to Gitolite failed or was rejected # Returns true otherwise def commit!(content, commit_message, last_commit) @@ -49,7 +49,7 @@ module Gitlab protected def can_edit?(last_commit) - current_last_commit = @project.repository.last_commit_for(ref, file_path).sha + current_last_commit = Gitlab::Git::Commit.last_for_path(@project.repository, ref, file_path).sha last_commit == current_last_commit end end diff --git a/lib/gitlab/satellite/merge_action.rb b/lib/gitlab/satellite/merge_action.rb index 832db6621c4..156483be8dd 100644 --- a/lib/gitlab/satellite/merge_action.rb +++ b/lib/gitlab/satellite/merge_action.rb @@ -5,48 +5,120 @@ module Gitlab attr_accessor :merge_request def initialize(user, merge_request) - super user, merge_request.project + super user, merge_request.target_project @merge_request = merge_request end # Checks if a merge request can be executed without user interaction def can_be_merged? in_locked_and_timed_satellite do |merge_repo| + prepare_satellite!(merge_repo) merge_in_satellite!(merge_repo) end end # Merges the source branch into the target branch in the satellite and - # pushes it back to Gitolite. - # It also removes the source branch if requested in the merge request. + # pushes it back to the repository. + # It also removes the source branch if requested in the merge request (and this is permitted by the merge request). # # Returns false if the merge produced conflicts - # Returns false if pushing from the satellite to Gitolite failed or was rejected + # Returns false if pushing from the satellite to the repository failed or was rejected # Returns true otherwise def merge! in_locked_and_timed_satellite do |merge_repo| + prepare_satellite!(merge_repo) if merge_in_satellite!(merge_repo) # push merge back to Gitolite # will raise CommandFailed when push fails - merge_repo.git.push({raise: true, timeout: true}, :origin, merge_request.target_branch) - + merge_repo.git.push(default_options, :origin, merge_request.target_branch) # remove source branch if merge_request.should_remove_source_branch && !project.root_ref?(merge_request.source_branch) # will raise CommandFailed when push fails - merge_repo.git.push({raise: true, timeout: true}, :origin, ":#{merge_request.source_branch}") + merge_repo.git.push(default_options, :origin, ":#{merge_request.source_branch}") end - # merge, push and branch removal successful true end end rescue Grit::Git::CommandFailed => ex - Gitlab::GitLogger.error(ex.message) - false + handle_exception(ex) end - private + # Get a raw diff of the source to the target + def diff_in_satellite + in_locked_and_timed_satellite do |merge_repo| + prepare_satellite!(merge_repo) + update_satellite_source_and_target!(merge_repo) + + if merge_request.for_fork? + diff = merge_repo.git.native(:diff, default_options, "origin/#{merge_request.target_branch}", "source/#{merge_request.source_branch}") + else + diff = merge_repo.git.native(:diff, default_options, "#{merge_request.target_branch}", "#{merge_request.source_branch}") + end + + return diff + end + rescue Grit::Git::CommandFailed => ex + handle_exception(ex) + end + + # Only show what is new in the source branch compared to the target branch, not the other way around. + # The line below with merge_base is equivalent to diff with three dots (git diff branch1...branch2) + # From the git documentation: "git diff A...B" is equivalent to "git diff $(git-merge-base A B) B" + def diffs_between_satellite + in_locked_and_timed_satellite do |merge_repo| + prepare_satellite!(merge_repo) + update_satellite_source_and_target!(merge_repo) + if merge_request.for_fork? + common_commit = merge_repo.git.native(:merge_base, default_options, ["origin/#{merge_request.target_branch}", "source/#{merge_request.source_branch}"]).strip + #this method doesn't take default options + diffs = merge_repo.diff(common_commit, "source/#{merge_request.source_branch}") + else + raise "Attempt to determine diffs between for a non forked merge request in satellite MergeRequest.id:[#{merge_request.id}]" + end + diffs = diffs.map { |diff| Gitlab::Git::Diff.new(diff) } + return diffs + end + rescue Grit::Git::CommandFailed => ex + handle_exception(ex) + end + + # Get commit as an email patch + def format_patch + in_locked_and_timed_satellite do |merge_repo| + prepare_satellite!(merge_repo) + update_satellite_source_and_target!(merge_repo) + + if (merge_request.for_fork?) + patch = merge_repo.git.format_patch(default_options({stdout: true}), "origin/#{merge_request.target_branch}..source/#{merge_request.source_branch}") + else + patch = merge_repo.git.format_patch(default_options({stdout: true}), "#{merge_request.target_branch}..#{merge_request.source_branch}") + end + + return patch + end + rescue Grit::Git::CommandFailed => ex + handle_exception(ex) + end + # Retrieve an array of commits between the source and the target + def commits_between + in_locked_and_timed_satellite do |merge_repo| + prepare_satellite!(merge_repo) + update_satellite_source_and_target!(merge_repo) + if (merge_request.for_fork?) + commits = merge_repo.commits_between("origin/#{merge_request.target_branch}", "source/#{merge_request.source_branch}") + else + raise "Attempt to determine commits between for a non forked merge request in satellite MergeRequest.id:[#{merge_request.id}]" + end + commits = commits.map { |commit| Gitlab::Git::Commit.new(commit, nil) } + return commits + end + rescue Grit::Git::CommandFailed => ex + handle_exception(ex) + end + + private # Merges the source_branch into the target_branch in the satellite. # # Note: it will clear out the satellite before doing anything @@ -54,18 +126,35 @@ module Gitlab # Returns false if the merge produced conflicts # Returns true otherwise def merge_in_satellite!(repo) - prepare_satellite!(repo) - - # create target branch in satellite at the corresponding commit from Gitolite - repo.git.checkout({raise: true, timeout: true, b: true}, merge_request.target_branch, "origin/#{merge_request.target_branch}") + update_satellite_source_and_target!(repo) - # merge the source branch from Gitolite into the satellite + # merge the source branch into the satellite # will raise CommandFailed when merge fails - repo.git.pull({raise: true, timeout: true, no_ff: true}, :origin, merge_request.source_branch) + if merge_request.for_fork? + repo.git.pull(default_options({no_ff: true}), 'source', merge_request.source_branch) + else + repo.git.pull(default_options({no_ff: true}), 'origin', merge_request.source_branch) + end rescue Grit::Git::CommandFailed => ex - Gitlab::GitLogger.error(ex.message) - false + handle_exception(ex) end + + # Assumes a satellite exists that is a fresh clone of the projects repo, prepares satellite for merges, diffs etc + def update_satellite_source_and_target!(repo) + if merge_request.for_fork? + repo.remote_add('source', merge_request.source_project.repository.path_to_repo) + repo.remote_fetch('source') + repo.git.checkout(default_options({b: true}), merge_request.target_branch, "origin/#{merge_request.target_branch}") + else + # We can't trust the input here being branch names, we can't always check it out because it could be a relative ref i.e. HEAD~3 + # we could actually remove the if true, because it should never ever happen (as long as the satellite has been prepared) + repo.git.checkout(default_options, "#{merge_request.source_branch}") + repo.git.checkout(default_options, "#{merge_request.target_branch}") + end + rescue Grit::Git::CommandFailed => ex + handle_exception(ex) + end + end end end diff --git a/lib/gitlab/satellite/satellite.rb b/lib/gitlab/satellite/satellite.rb index e7f7a7673b5..6cb7814fae5 100644 --- a/lib/gitlab/satellite/satellite.rb +++ b/lib/gitlab/satellite/satellite.rb @@ -1,5 +1,5 @@ module Gitlab - class SatelliteNotExistError < StandardError; end + class SatelliteNotExistError < StandardError; end module Satellite class Satellite @@ -24,8 +24,11 @@ module Gitlab def clear_and_update! raise_no_satellite unless exists? - delete_heads! + File.exists? path + @repo = nil clear_working_dir! + delete_heads! + remove_remotes! update_from_source! end @@ -55,16 +58,18 @@ module Gitlab raise_no_satellite unless exists? File.open(lock_file, "w+") do |f| - f.flock(File::LOCK_EX) - - Dir.chdir(path) do - return yield + begin + f.flock File::LOCK_EX + Dir.chdir(path) { return yield } + ensure + f.flock File::LOCK_UN end end end def lock_file - Rails.root.join("tmp", "satellite_#{project.id}.lock") + create_locks_dir unless File.exists?(lock_files_dir) + File.join(lock_files_dir, "satellite_#{project.id}.lock") end def path @@ -99,20 +104,44 @@ module Gitlab if heads.include? PARKING_BRANCH repo.git.checkout({}, PARKING_BRANCH) else - repo.git.checkout({b: true}, PARKING_BRANCH) + repo.git.checkout(default_options({b: true}), PARKING_BRANCH) end # remove the parking branch from the list of heads ... heads.delete(PARKING_BRANCH) # ... and delete all others - heads.each { |head| repo.git.branch({D: true}, head) } + heads.each { |head| repo.git.branch(default_options({D: true}), head) } + end + + # Deletes all remotes except origin + # + # This ensures we have no remote name clashes or issues updating branches when + # working with the satellite. + def remove_remotes! + remotes = repo.git.remote.split(' ') + remotes.delete('origin') + remotes.each { |name| repo.git.remote(default_options,'rm', name)} end # Updates the satellite from Gitolite # # Note: this will only update remote branches (i.e. origin/*) def update_from_source! - repo.git.fetch({timeout: true}, :origin) + repo.git.fetch(default_options, :origin) + end + + def default_options(options = {}) + {raise: true, timeout: true}.merge(options) + end + + # Create directory for storing + # satellites lock files + def create_locks_dir + FileUtils.mkdir_p(lock_files_dir) + end + + def lock_files_dir + @lock_files_dir ||= File.join(Gitlab.config.satellites.path, "tmp") end end end diff --git a/lib/gitlab/theme.rb b/lib/gitlab/theme.rb index 7f833867e39..89604162304 100644 --- a/lib/gitlab/theme.rb +++ b/lib/gitlab/theme.rb @@ -1,12 +1,18 @@ module Gitlab class Theme + BASIC = 1 + MARS = 2 + MODERN = 3 + GRAY = 4 + COLOR = 5 + def self.css_class_by_id(id) themes = { - 1 => "ui_basic", - 2 => "ui_mars", - 3 => "ui_modern", - 4 => "ui_gray", - 5 => "ui_color" + BASIC => "ui_basic", + MARS => "ui_mars", + MODERN => "ui_modern", + GRAY => "ui_gray", + COLOR => "ui_color" } id ||= 1 diff --git a/lib/gitlab/user_team_manager.rb b/lib/gitlab/user_team_manager.rb deleted file mode 100644 index a8ff4a3d94d..00000000000 --- a/lib/gitlab/user_team_manager.rb +++ /dev/null @@ -1,135 +0,0 @@ -# UserTeamManager class -# -# Used for manage User teams with project repositories -module Gitlab - class UserTeamManager - class << self - def assign(team, project, access) - project = Project.find(project) unless project.is_a? Project - searched_project = team.user_team_project_relationships.find_by_project_id(project.id) - - unless searched_project.present? - team.user_team_project_relationships.create(project_id: project.id, greatest_access: access) - update_team_users_access_in_project(team, project) - end - end - - def resign(team, project) - project = Project.find(project) unless project.is_a? Project - - team.user_team_project_relationships.with_project(project).destroy_all - - update_team_users_access_in_project(team, project) - end - - def update_team_user_membership(team, member, options) - updates = {} - - if options[:default_projects_access] && options[:default_projects_access] != team.default_projects_access(member) - updates[:permission] = options[:default_projects_access] - end - - if options[:group_admin].to_s != team.admin?(member).to_s - updates[:group_admin] = options[:group_admin].present? - end - - unless updates.blank? - user_team_relationship = team.user_team_user_relationships.find_by_user_id(member) - if user_team_relationship.update_attributes(updates) - if updates[:permission] - rebuild_project_permissions_to_member(team, member) - end - true - else - false - end - else - true - end - end - - def update_project_greates_access(team, project, permission) - project_relation = team.user_team_project_relationships.find_by_project_id(project) - if permission != team.max_project_access(project) - if project_relation.update_attributes(greatest_access: permission) - update_team_users_access_in_project(team, project) - true - else - false - end - else - true - end - end - - def rebuild_project_permissions_to_member(team, member) - team.projects.each do |project| - update_team_user_access_in_project(team, member, project) - end - end - - def update_team_users_access_in_project(team, project) - members = team.members - members.each do |member| - update_team_user_access_in_project(team, member, project) - end - end - - def update_team_user_access_in_project(team, user, project) - granted_access = max_teams_member_permission_in_project(user, project) - - project_team_user = UsersProject.find_by_user_id_and_project_id(user.id, project.id) - project_team_user.destroy if project_team_user.present? - - # project_team_user.project_access != granted_access - project.team << [user, granted_access] if granted_access > 0 - end - - def max_teams_member_permission_in_project(user, project, teams = nil) - result_access = 0 - - user_teams = project.user_teams.with_member(user) - - teams ||= user_teams - - if teams.any? - teams.each do |team| - granted_access = max_team_member_permission_in_project(team, user, project) - result_access = [granted_access, result_access].max - end - end - result_access - end - - def max_team_member_permission_in_project(team, user, project) - member_access = team.default_projects_access(user) - team_access = team.user_team_project_relationships.find_by_project_id(project.id).greatest_access - - [team_access, member_access].min - end - - def add_member_into_team(team, user, access, admin) - user = User.find(user) unless user.is_a? User - - team.user_team_user_relationships.create(user_id: user.id, permission: access, group_admin: admin) - team.projects.each do |project| - update_team_user_access_in_project(team, user, project) - end - end - - def remove_member_from_team(team, user) - user = User.find(user) unless user.is_a? User - - team.user_team_user_relationships.with_user(user).destroy_all - other_teams = [] - team.projects.each do |project| - other_teams << project.user_teams.with_member(user) - end - other_teams.uniq - unless other_teams.any? - UsersProject.in_projects(team.projects).with_user(user).destroy_all - end - end - end - end -end diff --git a/lib/gitlab/version_info.rb b/lib/gitlab/version_info.rb new file mode 100644 index 00000000000..6ee41e85cc9 --- /dev/null +++ b/lib/gitlab/version_info.rb @@ -0,0 +1,54 @@ +module Gitlab + class VersionInfo + include Comparable + + attr_reader :major, :minor, :patch + + def self.parse(str) + if str && m = str.match(/(\d+)\.(\d+)\.(\d+)/) + VersionInfo.new(m[1].to_i, m[2].to_i, m[3].to_i) + else + VersionInfo.new + end + end + + def initialize(major = 0, minor = 0, patch = 0) + @major = major + @minor = minor + @patch = patch + end + + def <=>(other) + return unless other.is_a? VersionInfo + return unless valid? && other.valid? + + if other.major < @major + 1 + elsif @major < other.major + -1 + elsif other.minor < @minor + 1 + elsif @minor < other.minor + -1 + elsif other.patch < @patch + 1 + elsif @patch < other.patch + -1 + else + 0 + end + end + + def to_s + if valid? + "%d.%d.%d" % [@major, @minor, @patch] + else + "Unknown" + end + end + + def valid? + @major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0 + end + end +end diff --git a/lib/gitolited.rb b/lib/gitolited.rb deleted file mode 100644 index a7fc4148106..00000000000 --- a/lib/gitolited.rb +++ /dev/null @@ -1,11 +0,0 @@ -# == Gitolited mixin -# -# Provide a shortcut to Gitlab::Shell instance by gitlab_shell -# -# Used by Project, UsersProject, etc -# -module Gitolited - def gitlab_shell - Gitlab::Shell.new - end -end diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index 4f2c86e2d41..d9c2d3b626d 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -11,7 +11,8 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML def block_code(code, language) options = { options: {encoding: 'utf-8'} } - options.merge!(lexer: language.downcase) if Pygments::Lexer.find(language) + lexer = Pygments::Lexer.find(language) # language can be an alias + options.merge!(lexer: lexer.aliases[0].downcase) if lexer # downcase is required # New lines are placed to fix an rendering issue # with code wrapped inside <h1> tag for next case: diff --git a/lib/support/deploy/deploy.sh b/lib/support/deploy/deploy.sh new file mode 100755 index 00000000000..0d2f8418bcf --- /dev/null +++ b/lib/support/deploy/deploy.sh @@ -0,0 +1,44 @@ +# This is deploy script we use to update staging server +# You can always modify it for your needs :) + +# If any command return non-zero status - stop deploy +set -e + +echo 'Deploy: Stoping sidekiq..' +cd /home/git/gitlab/ && sudo -u git -H bundle exec rake sidekiq:stop RAILS_ENV=production + +echo 'Deploy: Show deploy index page' +sudo -u git -H cp /home/git/gitlab/public/deploy.html /home/git/gitlab/public/index.html + +echo 'Deploy: Starting backup...' +cd /home/git/gitlab/ && sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production + +echo 'Deploy: Stop GitLab server' +sudo service gitlab stop + +echo 'Deploy: Get latest code' +cd /home/git/gitlab/ + +# clean working directory +sudo -u git -H git stash + +# change branch to +sudo -u git -H git pull origin master + +echo 'Deploy: Bundle and migrate' + +# change it to your needs +sudo -u git -H bundle --without postgres + +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# return stashed changes (if necessary) +# sudo -u git -H git stash pop + + +echo 'Deploy: Starting GitLab server...' +sudo service gitlab start + +sleep 10 +sudo -u git -H rm /home/git/gitlab/public/index.html +echo 'Deploy: Done' diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab new file mode 100755 index 00000000000..0248284f8d5 --- /dev/null +++ b/lib/support/init.d/gitlab @@ -0,0 +1,262 @@ +#! /bin/sh + +# GITLAB +# Maintainer: @randx +# Authors: rovanion.luckey@gmail.com, @randx +# App Version: 6.0 + +### BEGIN INIT INFO +# Provides: gitlab +# Required-Start: $local_fs $remote_fs $network $syslog redis-server +# Required-Stop: $local_fs $remote_fs $network $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: GitLab git repository management +# Description: GitLab git repository management +### END INIT INFO + +### Environment variables +RAILS_ENV="production" + +# Script variable names should be lower-case not to conflict with internal +# /bin/sh variables such as PATH, EDITOR or SHELL. +app_root="/home/git/gitlab" +app_user="git" +unicorn_conf="$app_root/config/unicorn.rb" +pid_path="$app_root/tmp/pids" +socket_path="$app_root/tmp/sockets" +web_server_pid_path="$pid_path/unicorn.pid" +sidekiq_pid_path="$pid_path/sidekiq.pid" + + + +### Here ends user configuration ### + + +# Switch to the app_user if it is not he/she who is running the script. +if [ "$USER" != "$app_user" ]; then + sudo -u "$app_user" -H -i $0 "$@"; exit; +fi + +# Switch to the gitlab path, if it fails exit with an error. +if ! cd "$app_root" ; then + echo "Failed to cd into $app_root, exiting!"; exit 1 +fi + +### Init Script functions + +check_pids(){ + if ! mkdir -p "$pid_path"; then + echo "Could not create the path $pid_path needed to store the pids." + exit 1 + fi + # If there exists a file which should hold the value of the Unicorn pid: read it. + if [ -f "$web_server_pid_path" ]; then + wpid=$(cat "$web_server_pid_path") + else + wpid=0 + fi + if [ -f "$sidekiq_pid_path" ]; then + spid=$(cat "$sidekiq_pid_path") + else + spid=0 + fi +} + +# We use the pids in so many parts of the script it makes sense to always check them. +# Only after start() is run should the pids change. Sidekiq sets it's own pid. +check_pids + + +# Checks whether the different parts of the service are already running or not. +check_status(){ + check_pids + # If the web server is running kill -0 $wpid returns true, or rather 0. + # Checks of *_status should only check for == 0 or != 0, never anything else. + if [ $wpid -ne 0 ]; then + kill -0 "$wpid" 2>/dev/null + web_status="$?" + else + web_status="-1" + fi + if [ $spid -ne 0 ]; then + kill -0 "$spid" 2>/dev/null + sidekiq_status="$?" + else + sidekiq_status="-1" + fi +} + +# Check for stale pids and remove them if necessary +check_stale_pids(){ + check_status + # If there is a pid it is something else than 0, the service is running if + # *_status is == 0. + if [ "$wpid" != "0" -a "$web_status" != "0" ]; then + echo "Removing stale Unicorn web server pid. This is most likely caused by the web server crashing the last time it ran." + if ! rm "$web_server_pid_path"; then + echo "Unable to remove stale pid, exiting" + exit 1 + fi + fi + if [ "$spid" != "0" -a "$sidekiq_status" != "0" ]; then + echo "Removing stale Sidekiq web server pid. This is most likely caused by the Sidekiq crashing the last time it ran." + if ! rm "$sidekiq_pid_path"; then + echo "Unable to remove stale pid, exiting" + exit 1 + fi + fi +} + +# If no parts of the service is running, bail out. +exit_if_not_running(){ + check_stale_pids + if [ "$web_status" != "0" -a "$sidekiq_status" != "0" ]; then + echo "GitLab is not running." + exit + fi +} + +# Starts Unicorn and Sidekiq. +start() { + check_stale_pids + + # Then check if the service is running. If it is: don't start again. + if [ "$web_status" = "0" ]; then + echo "The Unicorn web server already running with pid $wpid, not restarting." + else + echo "Starting the GitLab Unicorn web server..." + # Remove old socket if it exists + rm -f "$socket_path"/gitlab.socket 2>/dev/null + # Start the webserver + bundle exec unicorn_rails -D -c "$unicorn_conf" -E "$RAILS_ENV" + fi + + # If sidekiq is already running, don't start it again. + if [ "$sidekiq_status" = "0" ]; then + echo "The Sidekiq job dispatcher is already running with pid $spid, not restarting" + else + echo "Starting the GitLab Sidekiq event dispatcher..." + RAILS_ENV=$RAILS_ENV bundle exec rake sidekiq:start + # We are sleeping a bit here because sidekiq is slow at writing it's pid + sleep 2 + fi + + # Finally check the status to tell wether or not GitLab is running + status +} + +# Asks the Unicorn and the Sidekiq if they would be so kind as to stop, if not kills them. +stop() { + exit_if_not_running + # If the Unicorn web server is running, tell it to stop; + if [ "$web_status" = "0" ]; then + kill -QUIT "$wpid" & + echo "Stopping the GitLab Unicorn web server..." + stopping=true + else + echo "The Unicorn web was not running, doing nothing." + fi + # And do the same thing for the Sidekiq. + if [ "$sidekiq_status" = "0" ]; then + printf "Stopping Sidekiq job dispatcher." + RAILS_ENV=$RAILS_ENV bundle exec rake sidekiq:stop & + stopping=true + else + echo "The Sidekiq was not running, must have run out of breath." + fi + + + # If something needs to be stopped, lets wait for it to stop. Never use SIGKILL in a script. + while [ "$stopping" = "true" ]; do + sleep 1 + check_status + if [ "$web_status" = "0" -o "$sidekiq_status" = "0" ]; then + printf "." + else + printf "\n" + break + fi + done + sleep 1 + # Cleaning up unused pids + rm "$web_server_pid_path" 2>/dev/null + # rm "$sidekiq_pid_path" # Sidekiq seems to be cleaning up it's own pid. + + status +} + +# Returns the status of GitLab and it's components +status() { + check_status + if [ "$web_status" != "0" -a "$sidekiq_status" != "0" ]; then + echo "GitLab is not running." + return + fi + if [ "$web_status" = "0" ]; then + echo "The GitLab Unicorn webserver with pid $wpid is running." + else + printf "The GitLab Unicorn webserver is \033[31mnot running\033[0m.\n" + fi + if [ "$sidekiq_status" = "0" ]; then + echo "The GitLab Sidekiq job dispatcher with pid $spid is running." + else + printf "The GitLab Sidekiq job dispatcher is \033[31mnot running\033[0m.\n" + fi + if [ "$web_status" = "0" -a "$sidekiq_status" = "0" ]; then + printf "GitLab and all its components are \033[32mup and running\033[0m.\n" + fi +} + +reload(){ + exit_if_not_running + if [ "$wpid" = "0" ];then + echo "The GitLab Unicorn Web server is not running thus its configuration can't be reloaded." + exit 1 + fi + printf "Reloading GitLab Unicorn configuration... " + kill -USR2 "$wpid" + echo "Done." + echo "Restarting GitLab Sidekiq since it isn't capable of reloading its config..." + RAILS_ENV=$RAILS_ENV bundle exec rake sidekiq:stop + echo "Starting Sidekiq..." + RAILS_ENV=$RAILS_ENV bundle exec rake sidekiq:start + # Waiting 2 seconds for sidekiq to write it. + sleep 2 + status +} + +restart(){ + check_status + if [ "$web_status" = "0" -o "$sidekiq_status" = "0" ]; then + stop + fi + start +} + + +## Finally the input handling. + +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + restart + ;; + reload|force-reload) + reload + ;; + status) + status + ;; + *) + echo "Usage: service gitlab {start|stop|restart|reload|status}" + exit 1 + ;; +esac + +exit diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab new file mode 100644 index 00000000000..3e929c52990 --- /dev/null +++ b/lib/support/nginx/gitlab @@ -0,0 +1,39 @@ +# GITLAB +# Maintainer: @randx +# App Version: 5.0 + +upstream gitlab { + server unix:/home/git/gitlab/tmp/sockets/gitlab.socket; +} + +server { + listen *:80 default_server; # e.g., listen 192.168.1.1:80; In most cases *:80 is a good idea + server_name YOUR_SERVER_FQDN; # e.g., server_name source.example.com; + server_tokens off; # don't show the version number, a security best practice + root /home/git/gitlab/public; + + # individual nginx logs for this gitlab vhost + access_log /var/log/nginx/gitlab_access.log; + error_log /var/log/nginx/gitlab_error.log; + + location / { + # serve static files from defined root folder;. + # @gitlab is a named location for the upstream fallback, see below + try_files $uri $uri/index.html $uri.html @gitlab; + } + + # if a file, which is not found in the root folder is requested, + # then the proxy pass the request to the upsteam (gitlab unicorn) + location @gitlab { + proxy_read_timeout 300; # https://github.com/gitlabhq/gitlabhq/issues/694 + proxy_connect_timeout 300; # https://github.com/gitlabhq/gitlabhq/issues/694 + proxy_redirect off; + + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + + proxy_pass http://gitlab; + } +} + diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake new file mode 100644 index 00000000000..8320b9b2576 --- /dev/null +++ b/lib/tasks/cache.rake @@ -0,0 +1,6 @@ +namespace :cache do + desc "GITLAB | Clear redis cache" + task :clear => :environment do + Rails.cache.clear + end +end diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake new file mode 100644 index 00000000000..7d3602211c1 --- /dev/null +++ b/lib/tasks/dev.rake @@ -0,0 +1,10 @@ +namespace :dev do + desc "GITLAB | Setup developer environment (db, fixtures)" + task :setup => :environment do + ENV['force'] = 'yes' + Rake::Task["db:setup"].invoke + Rake::Task["db:seed_fu"].invoke + Rake::Task["gitlab:shell:setup"].invoke + end +end + diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 214ce720e7a..2eff1260b61 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -4,210 +4,75 @@ namespace :gitlab do namespace :backup do # Create backup of GitLab system desc "GITLAB | Create a backup of the GitLab system" - task :create => :environment do + task create: :environment do warn_user_is_not_gitlab Rake::Task["gitlab:backup:db:create"].invoke Rake::Task["gitlab:backup:repo:create"].invoke + Rake::Task["gitlab:backup:uploads:create"].invoke - Dir.chdir(Gitlab.config.backup.path) - - # saving additional informations - s = {} - s[:db_version] = "#{ActiveRecord::Migrator.current_version}" - s[:backup_created_at] = "#{Time.now}" - s[:gitlab_version] = %x{git rev-parse HEAD}.gsub(/\n/,"") - s[:tar_version] = %x{tar --version | head -1}.gsub(/\n/,"") - - File.open("#{Gitlab.config.backup.path}/backup_information.yml", "w+") do |file| - file << s.to_yaml.gsub(/^---\n/,'') - end - - # create archive - print "Creating backup archive: #{Time.now.to_i}_gitlab_backup.tar ... " - if Kernel.system("tar -cf #{Time.now.to_i}_gitlab_backup.tar repositories/ db/ backup_information.yml") - puts "done".green - else - puts "failed".red - end - - # cleanup: remove tmp files - print "Deleting tmp directories ... " - if Kernel.system("rm -rf repositories/ db/ backup_information.yml") - puts "done".green - else - puts "failed".red - end - - # delete backups - print "Deleting old backups ... " - if Gitlab.config.backup.keep_time > 0 - file_list = Dir.glob("*_gitlab_backup.tar").map { |f| f.split(/_/).first.to_i } - file_list.sort.each do |timestamp| - if Time.at(timestamp) < (Time.now - Gitlab.config.backup.keep_time) - %x{rm #{timestamp}_gitlab_backup.tar} - end - end - puts "done".green - else - puts "skipping".yellow - end + backup = Backup::Manager.new + backup.pack + backup.cleanup + backup.remove_old end # Restore backup of GitLab system desc "GITLAB | Restore a previously created backup" - task :restore => :environment do + task restore: :environment do warn_user_is_not_gitlab - Dir.chdir(Gitlab.config.backup.path) - - # 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" - exit 1 - end - - tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_backup.tar") - - unless File.exists?(tar_file) - puts "The specified backup doesn't exist!" - exit 1 - end - - print "Unpacking backup ... " - unless Kernel.system("tar -xf #{tar_file}") - puts "failed".red - exit 1 - else - 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 - if settings[:gitlab_version] != %x{git rev-parse HEAD}.gsub(/\n/,"") - puts "GitLab version mismatch:".red - puts " Your current HEAD differs from the HEAD in the backup!".red - puts " Please switch to the following revision and try again:".red - puts " revision: #{settings[:gitlab_version]}".red - exit 1 - end + backup = Backup::Manager.new + backup.unpack Rake::Task["gitlab:backup:db:restore"].invoke Rake::Task["gitlab:backup:repo:restore"].invoke + Rake::Task["gitlab:backup:uploads:restore"].invoke + Rake::Task["gitlab:shell:setup"].invoke - # cleanup: remove tmp files - print "Deleting tmp directories ... " - if Kernel.system("rm -rf repositories/ db/ backup_information.yml") - puts "done".green - else - puts "failed".red - end + backup.cleanup end - ################################################################################ - ################################# invoked tasks ################################ - - ################################# REPOSITORIES ################################# - namespace :repo do - task :create => :environment do - backup_path_repo = File.join(Gitlab.config.backup.path, "repositories") - FileUtils.mkdir_p(backup_path_repo) until Dir.exists?(backup_path_repo) + task create: :environment do puts "Dumping repositories ...".blue - - Project.find_each(:batch_size => 1000) do |project| - print " * #{project.path_with_namespace} ... " - - if project.empty_repo? - puts "[SKIPPED]".cyan - next - end - - # Create namespace dir if missing - FileUtils.mkdir_p(File.join(backup_path_repo, project.namespace.path)) if project.namespace - - # Build a destination path for backup - path_to_bundle = File.join(backup_path_repo, project.path_with_namespace + ".bundle") - - if Kernel.system("cd #{project.repository.path_to_repo} > /dev/null 2>&1 && git bundle create #{path_to_bundle} --all > /dev/null 2>&1") - puts "[DONE]".green - else - puts "[FAILED]".red - end - end + Backup::Repository.new.dump + puts "done".green end - task :restore => :environment do - backup_path_repo = File.join(Gitlab.config.backup.path, "repositories") - repos_path = Gitlab.config.gitlab_shell.repos_path - - puts "Restoring repositories ... " - - Project.find_each(:batch_size => 1000) do |project| - print "#{project.path_with_namespace} ... " - - if project.namespace - project.namespace.ensure_dir_exist - end - - # Build a backup path - path_to_bundle = File.join(backup_path_repo, project.path_with_namespace + ".bundle") - - if Kernel.system("git clone --bare #{path_to_bundle} #{project.repository.path_to_repo} > /dev/null 2>&1") - puts "[DONE]".green - else - puts "[FAILED]".red - end - end + task restore: :environment do + puts "Restoring repositories ...".blue + Backup::Repository.new.restore + puts "done".green end end - ###################################### DB ###################################### - namespace :db do - task :create => :environment do - backup_path_db = File.join(Gitlab.config.backup.path, "db") - FileUtils.mkdir_p(backup_path_db) unless Dir.exists?(backup_path_db) - - puts "Dumping database tables ... ".blue - ActiveRecord::Base.connection.tables.each do |tbl| - print " * #{tbl.yellow} ... " - count = 1 - File.open(File.join(backup_path_db, tbl + ".yml"), "w+") do |file| - ActiveRecord::Base.connection.select_all("SELECT * FROM `#{tbl}`").each do |line| - line.delete_if{|k,v| v.blank?} - output = {tbl + '_' + count.to_s => line} - file << output.to_yaml.gsub(/^---\n/,'') + "\n" - count += 1 - end - puts "done".green - end - end + task create: :environment do + puts "Dumping database ... ".blue + Backup::Database.new.dump + puts "done".green end - task :restore => :environment do - backup_path_db = File.join(Gitlab.config.backup.path, "db") + task restore: :environment do + puts "Restoring database ... ".blue + Backup::Database.new.restore + puts "done".green + end + end - puts "Restoring database tables (loading fixtures) ... " - Rake::Task["db:reset"].invoke + namespace :uploads do + task create: :environment do + puts "Dumping uploads ... ".blue + Backup::Uploads.new.dump + puts "done".green + end - Dir.glob(File.join(backup_path_db, "*.yml") ).each do |dir| - fixture_file = File.basename(dir, ".*" ) - print "#{fixture_file.yellow} ... " - if File.size(dir) > 0 - ActiveRecord::Fixtures.create_fixtures(backup_path_db, fixture_file) - puts "done".green - else - puts "skipping".yellow - end - end + task restore: :environment do + puts "Restoring uploads ... ".blue + Backup::Uploads.new.restore + puts "done".green end end - end # namespace end: backup end # namespace end: gitlab diff --git a/lib/tasks/gitlab/bulk_add_permission.rake b/lib/tasks/gitlab/bulk_add_permission.rake index eb1a7559dbd..c270232edba 100644 --- a/lib/tasks/gitlab/bulk_add_permission.rake +++ b/lib/tasks/gitlab/bulk_add_permission.rake @@ -1,9 +1,9 @@ namespace :gitlab do namespace :import do desc "GITLAB | Add all users to all projects (admin users are added as masters)" - task :all_users_to_all_projects => :environment do |t, args| - user_ids = User.where(:admin => false).pluck(:id) - admin_ids = User.where(:admin => true).pluck(:id) + task all_users_to_all_projects: :environment do |t, args| + user_ids = User.where(admin: false).pluck(:id) + admin_ids = User.where(admin: true).pluck(:id) projects_ids = Project.pluck(:id) puts "Importing #{user_ids.size} users into #{projects_ids.size} projects" @@ -21,4 +21,4 @@ namespace :gitlab do UsersProject.add_users_into_projects(project_ids, Array.wrap(user.id), UsersProject::DEVELOPER) end end -end
\ No newline at end of file +end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 6a138396087..6e2a59f62ac 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -22,7 +22,10 @@ namespace :gitlab do check_tmp_writable check_init_script_exists check_init_script_up_to_date + check_projects_have_namespace check_satellites_exist + check_redis_version + check_git_version finished_checking "GitLab" end @@ -136,13 +139,15 @@ namespace :gitlab do def check_init_script_up_to_date print "Init script up-to-date? ... " + recipe_path = Rails.root.join("lib/support/init.d/", "gitlab") script_path = "/etc/init.d/gitlab" + unless File.exists?(script_path) puts "can't check because of previous errors".magenta return end - recipe_content = `curl https://raw.github.com/gitlabhq/gitlab-recipes/master/init.d/gitlab 2>/dev/null` + recipe_content = File.read(recipe_path) script_content = File.read(script_path) if recipe_content == script_content @@ -217,7 +222,7 @@ namespace :gitlab do puts "no".red try_fixing_it( "sudo chown -R gitlab #{log_path}", - "sudo chmod -R rwX #{log_path}" + "sudo chmod -R u+rwX #{log_path}" ) for_more_information( see_installation_guide_section "GitLab" @@ -237,7 +242,7 @@ namespace :gitlab do puts "no".red try_fixing_it( "sudo chown -R gitlab #{tmp_path}", - "sudo chmod -R rwX #{tmp_path}" + "sudo chmod -R u+rwX #{tmp_path}" ) for_more_information( see_installation_guide_section "GitLab" @@ -245,6 +250,23 @@ namespace :gitlab do fix_and_rerun end end + + def check_redis_version + print "Redis version >= 2.0.0? ... " + + if run_and_match("redis-cli --version", /redis-cli 2.\d.\d/) + puts "yes".green + else + puts "no".red + try_fixing_it( + "Update your redis server to a version >= 2.0.0" + ) + for_more_information( + "gitlab-public-wiki/wiki/Trouble-Shooting-Guide in section sidekiq" + ) + fix_and_rerun + end + end end @@ -255,7 +277,6 @@ namespace :gitlab do warn_user_is_not_gitlab start_checking "Environment" - check_issue_1059_shell_profile_error check_gitlab_git_config check_python2_exists check_python2_version @@ -294,30 +315,6 @@ namespace :gitlab do end end - # see https://github.com/gitlabhq/gitlabhq/issues/1059 - def check_issue_1059_shell_profile_error - gitlab_shell_ssh_user = Gitlab.config.gitlab_shell.ssh_user - print "Has no \"-e\" in ~#{gitlab_shell_ssh_user}/.profile ... " - - profile_file = File.join(gitlab_shell_user_home, ".profile") - - unless File.read(profile_file) =~ /^-e PATH/ - puts "yes".green - else - puts "no".red - try_fixing_it( - "Open #{profile_file}", - "Find the line starting with \"-e PATH\"", - "Remove \"-e \" so the line starts with PATH" - ) - for_more_information( - see_installation_guide_section("Gitlab Shell"), - "https://github.com/gitlabhq/gitlabhq/issues/1059" - ) - fix_and_rerun - end - end - def check_python2_exists print "Has python2? ... " @@ -368,19 +365,21 @@ namespace :gitlab do namespace :gitlab_shell do - desc "GITLAB | Check the configuration of Gitlab Shell" + desc "GITLAB | Check the configuration of GitLab Shell" task check: :environment do warn_user_is_not_gitlab - start_checking "Gitlab Shell" + start_checking "GitLab Shell" + check_gitlab_shell check_repo_base_exists check_repo_base_is_not_symlink check_repo_base_user_and_group check_repo_base_permissions - check_post_receive_hook_is_up_to_date - check_repos_post_receive_hooks_is_link + check_update_hook_is_up_to_date + check_repos_update_hooks_is_link + check_gitlab_shell_self_test - finished_checking "Gitlab Shell" + finished_checking "GitLab Shell" end @@ -388,10 +387,10 @@ namespace :gitlab do ######################## - def check_post_receive_hook_is_up_to_date - print "post-receive hook up-to-date? ... " + def check_update_hook_is_up_to_date + print "update hook up-to-date? ... " - hook_file = "post-receive" + hook_file = "update" gitlab_shell_hooks_path = Gitlab.config.gitlab_shell.hooks_path gitlab_shell_hook_file = File.join(gitlab_shell_hooks_path, hook_file) gitlab_shell_ssh_user = Gitlab.config.gitlab_shell.ssh_user @@ -415,12 +414,12 @@ namespace :gitlab do puts "no".red puts "#{repo_base_path} is missing".red try_fixing_it( - "This should have been created when setting up Gitlab Shell.", + "This should have been created when setting up GitLab Shell.", "Make sure it's set correctly in config/gitlab.yml", - "Make sure Gitlab Shell is installed correctly." + "Make sure GitLab Shell is installed correctly." ) for_more_information( - see_installation_guide_section "Gitlab Shell" + see_installation_guide_section "GitLab Shell" ) fix_and_rerun end @@ -465,7 +464,7 @@ namespace :gitlab do "find #{repo_base_path} -type d -print0 | sudo xargs -0 chmod g+s" ) for_more_information( - see_installation_guide_section "Gitlab Shell" + see_installation_guide_section "GitLab Shell" ) fix_and_rerun end @@ -482,25 +481,27 @@ namespace :gitlab do return end - if File.stat(repo_base_path).uid == uid_for(gitlab_shell_ssh_user) && - File.stat(repo_base_path).gid == gid_for(gitlab_shell_owner_group) + uid = uid_for(gitlab_shell_ssh_user) + gid = gid_for(gitlab_shell_owner_group) + if File.stat(repo_base_path).uid == uid && File.stat(repo_base_path).gid == gid puts "yes".green else puts "no".red + puts " User id for #{gitlab_shell_ssh_user}: #{uid}. Groupd id for #{gitlab_shell_owner_group}: #{gid}".blue try_fixing_it( "sudo chown -R #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group} #{repo_base_path}" ) for_more_information( - see_installation_guide_section "Gitlab Shell" + see_installation_guide_section "GitLab Shell" ) fix_and_rerun end end - def check_repos_post_receive_hooks_is_link - print "post-receive hooks in repos are links: ... " + def check_repos_update_hooks_is_link + print "update hooks in repos are links: ... " - hook_file = "post-receive" + hook_file = "update" gitlab_shell_hooks_path = Gitlab.config.gitlab_shell.hooks_path gitlab_shell_hook_file = File.join(gitlab_shell_hooks_path, hook_file) gitlab_shell_ssh_user = Gitlab.config.gitlab_shell.ssh_user @@ -540,7 +541,7 @@ namespace :gitlab do File.realpath(project_hook_file) == File.realpath(gitlab_shell_hook_file) puts "ok".green else - puts "not a link to Gitlab Shell's hook".red + puts "not a link to GitLab Shell's hook".red try_fixing_it( "sudo -u #{gitlab_shell_ssh_user} ln -sf #{gitlab_shell_hook_file} #{project_hook_file}" ) @@ -553,6 +554,49 @@ namespace :gitlab do end end + def check_gitlab_shell_self_test + gitlab_shell_repo_base = File.expand_path('gitlab-shell', gitlab_shell_user_home) + check_cmd = File.expand_path('bin/check', gitlab_shell_repo_base) + puts "Running #{check_cmd}" + if system(check_cmd, chdir: gitlab_shell_repo_base) + puts 'gitlab-shell self-check successful'.green + else + puts 'gitlab-shell self-check failed'.red + try_fixing_it( + 'Make sure GitLab is running;', + 'Check the gitlab-shell configuration file:', + sudo_gitlab("editor #{File.expand_path('config.yml', gitlab_shell_repo_base)}") + ) + fix_and_rerun + end + end + + def check_projects_have_namespace + print "projects have namespace: ... " + + unless Project.count > 0 + puts "can't check, you have no projects".magenta + return + end + puts "" + + Project.find_each(batch_size: 100) do |project| + print "#{project.name_with_namespace.yellow} ... " + + if project.namespace + puts "yes".green + else + puts "no".red + try_fixing_it( + "Migrate global projects" + ) + for_more_information( + "doc/update/5.4-to-6.0.md in section \"#global-projects\"" + ) + fix_and_rerun + end + end + end # Helper methods ######################## @@ -582,6 +626,7 @@ namespace :gitlab do start_checking "Sidekiq" check_sidekiq_running + only_one_sidekiq_running finished_checking "Sidekiq" end @@ -593,7 +638,7 @@ namespace :gitlab do def check_sidekiq_running print "Running? ... " - if run_and_match("ps aux | grep -i sidekiq", /sidekiq \d\.\d\.\d.+$/) + if sidekiq_process_match puts "yes".green else puts "no".red @@ -607,6 +652,29 @@ namespace :gitlab do fix_and_rerun end end + + def only_one_sidekiq_running + sidekiq_match = sidekiq_process_match + return unless sidekiq_match + + print 'Number of Sidekiq processes ... ' + if sidekiq_match.length == 1 + puts '1'.green + else + puts "#{sidekiq_match.length}".red + try_fixing_it( + 'sudo service gitlab stop', + 'sudo pkill -f sidekiq', + 'sleep 10 && sudo pkill -9 -f sidekiq', + 'sudo service gitlab start' + ) + fix_and_rerun + end + end + + def sidekiq_process_match + run_and_match("ps ux | grep -i sidekiq", /(sidekiq \d+\.\d+\.\d+.+$)/) + end end @@ -658,4 +726,34 @@ namespace :gitlab do puts " #{step}" end end + + def check_gitlab_shell + required_version = Gitlab::VersionInfo.new(1, 7, 1) + current_version = Gitlab::VersionInfo.parse(gitlab_shell_version) + + print "GitLab Shell version >= #{required_version} ? ... " + if current_version.valid? && required_version <= current_version + puts "OK (#{current_version})".green + else + puts "FAIL. Please update gitlab-shell to #{required_version} from #{current_version}".red + end + end + + def check_git_version + required_version = Gitlab::VersionInfo.new(1, 7, 10) + current_version = Gitlab::VersionInfo.parse(run("#{Gitlab.config.git.bin_path} --version")) + + puts "Your git bin path is \"#{Gitlab.config.git.bin_path}\"" + print "Git version >= #{required_version} ? ... " + + if current_version.valid? && required_version <= current_version + puts "yes (#{current_version})".green + else + puts "no".red + try_fixing_it( + "Update your git to a version >= #{required_version} from #{current_version}" + ) + fix_and_rerun + end + end end diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index d8ee56e5523..4aaab11340f 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -1,7 +1,7 @@ namespace :gitlab do namespace :cleanup do desc "GITLAB | Cleanup | Clean namespaces" - task :dirs => :environment do + task dirs: :environment do warn_user_is_not_gitlab remove_flag = ENV['REMOVE'] @@ -43,8 +43,8 @@ namespace :gitlab do end end - desc "GITLAB | Cleanup | Clean respositories" - task :repos => :environment do + desc "GITLAB | Cleanup | Clean repositories" + task repos: :environment do warn_user_is_not_gitlab remove_flag = ENV['REMOVE'] diff --git a/lib/tasks/gitlab/enable_namespaces.rake b/lib/tasks/gitlab/enable_namespaces.rake index a33639a0013..927748c0fd5 100644 --- a/lib/tasks/gitlab/enable_namespaces.rake +++ b/lib/tasks/gitlab/enable_namespaces.rake @@ -42,7 +42,7 @@ namespace :gitlab do username = user.email.match(/^[^@]*/)[0] username.gsub!("+", ".") - # return username if no mathes + # return username if no matches return username unless User.find_by_username(username) # look for same username @@ -99,7 +99,7 @@ namespace :gitlab do end begin - Gitlab::ProjectMover.new(project, '', group.path).execute + project.transfer(group.path) puts "moved to #{new_path}".green rescue puts "failed moving to #{new_path}".red diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index bddbd7ef855..8fa89270854 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -2,53 +2,76 @@ namespace :gitlab do namespace :import do # How to use: # - # 1. copy your bare repos under git base_path + # 1. copy your bare repos under git repos_path # 2. run bundle exec rake gitlab:import:repos RAILS_ENV=production # # Notes: # * project owner will be a first admin # * existing projects will be skipped # - desc "GITLAB | Import bare repositories from git_host -> base_path into GitLab project instance" - task :repos => :environment do + desc "GITLAB | Import bare repositories from gitlab_shell -> repos_path into GitLab project instance" + task repos: :environment do git_base_path = Gitlab.config.gitlab_shell.repos_path - repos_to_import = Dir.glob(git_base_path + '/*') + repos_to_import = Dir.glob(git_base_path + '/**/*.git') namespaces = Namespace.pluck(:path) repos_to_import.each do |repo_path| - repo_name = File.basename repo_path + # strip repo base path + repo_path[0..git_base_path.length] = '' - # Skip if group or user - next if namespaces.include?(repo_name) + path = repo_path.sub(/\.git$/, '') + name = File.basename path + group_name = File.dirname path + group_name = nil if group_name == '.' - # skip if not git repo - next unless repo_name =~ /.git$/ + # Skip if group or user + next if namespaces.include?(name) - next if repo_name == 'gitolite-admin.git' + puts "Processing #{repo_path}".yellow - path = repo_name.sub(/\.git$/, '') + if path =~ /.wiki\Z/ + puts " * Skipping wiki repo" + next + end project = Project.find_with_namespace(path) - puts "Processing #{repo_name}".yellow - if project - puts " * #{project.name} (#{repo_name}) exists" + puts " * #{project.name} (#{repo_path}) exists" else user = User.admins.first project_params = { - :name => path, + name: name, + path: name } + # find group namespace + if group_name + group = Group.find_by_path(group_name) + # create group namespace + if !group + group = Group.new(:name => group_name) + group.path = group_name + group.owner = user + if group.save + puts " * Created Group #{group.name} (#{group.id})".green + else + puts " * Failed trying to create group #{group.name}".red + end + end + # set project group + project_params[:namespace_id] = group.id + end + project = Projects::CreateContext.new(user, project_params).execute if project.valid? - puts " * Created #{project.name} (#{repo_name})".green + puts " * Created #{project.name} (#{repo_path})".green else - puts " * Failed trying to create #{project.name} (#{repo_name})".red + puts " * Failed trying to create #{project.name} (#{repo_path})".red end end end diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake index c44016ef6e8..ea83efcd887 100644 --- a/lib/tasks/gitlab/info.rake +++ b/lib/tasks/gitlab/info.rake @@ -40,8 +40,8 @@ namespace :gitlab do puts "" puts "GitLab information".yellow - puts "Version:\t#{Gitlab::Version}" - puts "Revision:\t#{Gitlab::Revision}" + puts "Version:\t#{Gitlab::VERSION}" + puts "Revision:\t#{Gitlab::REVISION}" puts "Directory:\t#{Rails.root}" puts "DB Adapter:\t#{database_adapter}" puts "URL:\t\t#{Gitlab.config.gitlab.url}" @@ -54,7 +54,7 @@ namespace :gitlab do # check Gitolite version - gitlab_shell_version_file = "#{Gitlab.config.gitlab_shell.repos_path}/../gitlab-shell/VERSION" + gitlab_shell_version_file = "#{Gitlab.config.gitlab_shell.hooks_path}/../VERSION" if File.readable?(gitlab_shell_version_file) gitlab_shell_version = File.read(gitlab_shell_version_file) end diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake index bc0742564d0..2b730774e06 100644 --- a/lib/tasks/gitlab/setup.rake +++ b/lib/tasks/gitlab/setup.rake @@ -1,16 +1,18 @@ namespace :gitlab do desc "GITLAB | Setup production application" - task :setup => :environment do - setup + task setup: :environment do + setup_db end - def setup + def setup_db warn_user_is_not_gitlab - puts "This will create the necessary database tables and seed the database." - puts "You will lose any previous data stored in the database." - ask_to_continue - puts "" + unless ENV['force'] == 'yes' + puts "This will create the necessary database tables and seed the database." + puts "You will lose any previous data stored in the database." + ask_to_continue + puts "" + end Rake::Task["db:setup"].invoke Rake::Task["db:seed_fu"].invoke diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index 0ab8df1d094..0d7a390bc92 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -25,12 +25,14 @@ namespace :gitlab do def setup warn_user_is_not_gitlab - puts "This will rebuild an authorized_keys file." - puts "You will lose any data stored in /home/git/.ssh/authorized_keys." - ask_to_continue - puts "" + unless ENV['force'] == 'yes' + puts "This will rebuild an authorized_keys file." + puts "You will lose any data stored in authorized_keys file." + ask_to_continue + puts "" + end - system("echo '# Managed by gitlab-shell' > /home/git/.ssh/authorized_keys") + Gitlab::Shell.new.remove_all_keys Key.find_each(batch_size: 1000) do |key| if Gitlab::Shell.new.add_key(key.shell_id, key.key) diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake index cb4e34cc0d7..ac2c4577c77 100644 --- a/lib/tasks/gitlab/task_helpers.rake +++ b/lib/tasks/gitlab/task_helpers.rake @@ -16,7 +16,7 @@ namespace :gitlab do # Check which OS is running # # It will primarily use lsb_relase to determine the OS. - # It has fallbacks to Debian, SuSE and OS X. + # It has fallbacks to Debian, SuSE, OS X and systems running systemd. def os_name os_name = run("lsb_release -irs") os_name ||= if File.readable?('/etc/system-release') @@ -32,13 +32,16 @@ namespace :gitlab do os_name ||= if os_x_version = run("sw_vers -productVersion") "Mac OS X #{os_x_version}" end + os_name ||= if File.readable?('/etc/os-release') + File.read('/etc/os-release').match(/PRETTY_NAME=\"(.+)\"/)[1] + end os_name.try(:squish!) end # Prompt the user to input something # # message - the message to display before input - # choices - array of strings of acceptible answers or nil for any answer + # choices - array of strings of acceptable answers or nil for any answer # # Returns the user's answer def prompt(message, choices = nil) @@ -49,10 +52,10 @@ namespace :gitlab do answer end - # Runs the given command and matches the output agains the given pattern + # Runs the given command and matches the output against the given pattern # # Returns nil if nothing matched - # Retunrs the MatchData if the pattern matched + # Returns the MatchData if the pattern matched # # see also #run # see also String#match @@ -77,7 +80,11 @@ namespace :gitlab do end def gid_for(group_name) - Etc.getgrnam(group_name).gid + begin + Etc.getgrnam(group_name).gid + rescue ArgumentError # no group + "group #{group_name} doesn't exist" + end end def warn_user_is_not_gitlab diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake index ad1bfb2e4b3..03b3fc5ea20 100644 --- a/lib/tasks/gitlab/test.rake +++ b/lib/tasks/gitlab/test.rake @@ -1,4 +1,4 @@ namespace :gitlab do desc "GITLAB | Run both spinach and rspec" - task :test => ['spinach', 'spec'] + task test: ['spinach', 'spec'] end diff --git a/lib/tasks/migrate/migrate_iids.rake b/lib/tasks/migrate/migrate_iids.rake new file mode 100644 index 00000000000..33271e1a2bb --- /dev/null +++ b/lib/tasks/migrate/migrate_iids.rake @@ -0,0 +1,48 @@ +desc "GITLAB | Build internal ids for issues and merge requests" +task migrate_iids: :environment do + puts 'Issues'.yellow + Issue.where(iid: nil).find_each(batch_size: 100) do |issue| + begin + issue.set_iid + if issue.update_attribute(:iid, issue.iid) + print '.' + else + print 'F' + end + rescue + print 'F' + end + end + + puts 'done' + puts 'Merge Requests'.yellow + MergeRequest.where(iid: nil).find_each(batch_size: 100) do |mr| + begin + mr.set_iid + if mr.update_attribute(:iid, mr.iid) + print '.' + else + print 'F' + end + rescue => ex + print 'F' + end + end + + puts 'done' + puts 'Milestones'.yellow + Milestone.where(iid: nil).find_each(batch_size: 100) do |m| + begin + m.set_iid + if m.update_attribute(:iid, m.iid) + print '.' + else + print 'F' + end + rescue + print 'F' + end + end + + puts 'done' +end diff --git a/lib/tasks/sidekiq.rake b/lib/tasks/sidekiq.rake index cf99951e027..d0e9dfe46a1 100644 --- a/lib/tasks/sidekiq.rake +++ b/lib/tasks/sidekiq.rake @@ -1,19 +1,19 @@ namespace :sidekiq do desc "GITLAB | Stop sidekiq" task :stop do - run "bundle exec sidekiqctl stop #{pidfile}" + system "bundle exec sidekiqctl stop #{pidfile}" end desc "GITLAB | Start sidekiq" task :start do - run "nohup bundle exec sidekiq -q post_receive,mailer,system_hook,project_web_hook,gitlab_shell,common,default -e #{Rails.env} -P #{pidfile} >> #{Rails.root.join("log", "sidekiq.log")} 2>&1 &" + system "nohup bundle exec sidekiq -q post_receive,mailer,system_hook,project_web_hook,gitlab_shell,common,default -e #{Rails.env} -P #{pidfile} >> #{Rails.root.join("log", "sidekiq.log")} 2>&1 &" end - + desc "GITLAB | Start sidekiq with launchd on Mac OS X" task :launchd do - run "bundle exec sidekiq -q post_receive,mailer,system_hook,project_web_hook,gitlab_shell,common,default -e #{Rails.env} -P #{pidfile} >> #{Rails.root.join("log", "sidekiq.log")} 2>&1" + system "bundle exec sidekiq -q post_receive,mailer,system_hook,project_web_hook,gitlab_shell,common,default -e #{Rails.env} -P #{pidfile} >> #{Rails.root.join("log", "sidekiq.log")} 2>&1" end - + def pidfile Rails.root.join("tmp", "pids", "sidekiq.pid") end diff --git a/lib/tasks/travis.rake b/lib/tasks/travis.rake index 6b434830803..bc1b8aadbc5 100644 --- a/lib/tasks/travis.rake +++ b/lib/tasks/travis.rake @@ -1,5 +1,5 @@ desc "Travis run tests" -task :travis => [ +task travis: [ :spinach, :spec ] |