diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/api.rb | 1 | ||||
-rw-r--r-- | lib/api/entities.rb | 10 | ||||
-rw-r--r-- | lib/api/merge_requests.rb | 118 | ||||
-rw-r--r-- | lib/event_filter.rb | 68 | ||||
-rw-r--r-- | lib/gitlab/auth.rb | 4 | ||||
-rw-r--r-- | lib/gitlab/backend/gitolite_config.rb | 5 | ||||
-rw-r--r-- | lib/gitlab/backend/grack_auth.rb | 60 | ||||
-rw-r--r-- | lib/gitlab/file_editor.rb | 58 | ||||
-rw-r--r-- | lib/gitlab/graph/commit.rb | 48 | ||||
-rw-r--r-- | lib/gitlab/graph/json_builder.rb | 172 | ||||
-rw-r--r-- | lib/gitlab/graph_commit.rb | 185 | ||||
-rw-r--r-- | lib/gitlab/merge.rb | 65 | ||||
-rw-r--r-- | lib/gitlab/satellite.rb | 41 | ||||
-rw-r--r-- | lib/gitlab/satellite/action.rb | 46 | ||||
-rw-r--r-- | lib/gitlab/satellite/edit_file_action.rb | 57 | ||||
-rw-r--r-- | lib/gitlab/satellite/merge_action.rb | 71 | ||||
-rw-r--r-- | lib/gitlab/satellite/satellite.rb | 92 | ||||
-rw-r--r-- | lib/tasks/bulk_import.rake | 83 | ||||
-rw-r--r-- | lib/tasks/gitlab/import.rake | 54 | ||||
-rw-r--r-- | lib/tasks/gitlab/status.rake | 2 |
20 files changed, 789 insertions, 451 deletions
diff --git a/lib/api.rb b/lib/api.rb index 2890a8cc9e8..7a1845443e7 100644 --- a/lib/api.rb +++ b/lib/api.rb @@ -18,5 +18,6 @@ module Gitlab mount Issues mount Milestones mount Session + mount MergeRequests end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index ee693de699e..9e605a607a2 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -63,5 +63,15 @@ module Gitlab class SSHKey < Grape::Entity expose :id, :title, :key end + + class MergeRequest < Grape::Entity + expose :id, :target_branch, :source_branch, :project_id, :title, :closed, :merged + expose :author, :assignee, using: Entities::UserBasic + end + + class Note < Grape::Entity + expose :author, using: Entities::UserBasic + expose :note + end end end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb new file mode 100644 index 00000000000..d8f2c51293a --- /dev/null +++ b/lib/api/merge_requests.rb @@ -0,0 +1,118 @@ +module Gitlab + # MergeRequest API + class MergeRequests < Grape::API + before { authenticate! } + + resource :projects do + + # List merge requests + # + # Parameters: + # id (required) - The ID or code name of a project + # + # Example: + # GET /projects/:id/merge_requests + # + get ":id/merge_requests" do + authorize! :read_merge_request, user_project + + present paginate(user_project.merge_requests), with: Entities::MergeRequest + end + + # Show MR + # + # Parameters: + # id (required) - The ID or code name of a project + # merge_request_id (required) - The ID of MR + # + # Example: + # GET /projects/:id/merge_request/:merge_request_id + # + get ":id/merge_request/:merge_request_id" do + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + + authorize! :read_merge_request, merge_request + + present merge_request, with: Entities::MergeRequest + end + + # Create MR + # + # Parameters: + # + # id (required) - The ID or code name of a project + # source_branch (required) - The source branch + # target_branch (required) - The target branch + # assignee_id - Assignee user ID + # title (required) - Title of MR + # + # Example: + # POST /projects/:id/merge_requests + # + post ":id/merge_requests" do + authorize! :write_merge_request, user_project + + attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title] + merge_request = user_project.merge_requests.new(attrs) + merge_request.author = current_user + + if merge_request.save + merge_request.reload_code + present merge_request, with: Entities::MergeRequest + else + not_found! + end + end + + # Update MR + # + # Parameters: + # id (required) - The ID or code name of a project + # merge_request_id (required) - ID of MR + # source_branch - The source branch + # target_branch - The target branch + # assignee_id - Assignee user ID + # title - Title of MR + # closed - Status of MR. true - closed + # 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] + merge_request = user_project.merge_requests.find(params[:merge_request_id]) + + authorize! :modify_merge_request, merge_request + + if merge_request.update_attributes attrs + merge_request.reload_code + merge_request.mark_as_unchecked + present merge_request, with: Entities::MergeRequest + else + not_found! + end + end + + # Post comment to merge request + # + # Parameters: + # id (required) - The ID or code name of a project + # merge_request_id (required) - ID of MR + # note (required) - Text of comment + # Examples: + # POST /projects/:id/merge_request/:merge_request_id/comments + # + post ":id/merge_request/:merge_request_id/comments" do + 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 + + if note.save + present note, with: Entities::Note + else + not_found! + end + end + + end + end +end diff --git a/lib/event_filter.rb b/lib/event_filter.rb new file mode 100644 index 00000000000..14ab0193bf1 --- /dev/null +++ b/lib/event_filter.rb @@ -0,0 +1,68 @@ +class EventFilter + attr_accessor :params + + class << self + def default_filter + %w{ push issues merge_requests team} + end + + def push + 'push' + end + + def merged + 'merged' + end + + def comments + 'comments' + end + + def team + 'team' + end + end + + def initialize params + @params = if params + params.dup + else + []#EventFilter.default_filter + end + end + + def apply_filter events + return events unless params.present? + + filter = params.dup + + actions = [] + actions << Event::Pushed if filter.include? 'push' + actions << Event::Merged if filter.include? 'merged' + + if filter.include? 'team' + actions << Event::Joined + actions << Event::Left + end + + actions << Event::Commented if filter.include? 'comments' + + events = events.where(action: actions) + end + + def options key + filter = params.dup + + if filter.include? key + filter.delete key + else + filter << key + end + + filter + end + + def active? key + params.include? key + end +end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 500cb64df48..5a24c5d01b5 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -48,9 +48,13 @@ module Gitlab 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 else if Gitlab.config.omniauth['allow_single_sign_on'] @user = create_from_omniauth(auth) diff --git a/lib/gitlab/backend/gitolite_config.rb b/lib/gitlab/backend/gitolite_config.rb index d988164d439..1bef19a2a79 100644 --- a/lib/gitlab/backend/gitolite_config.rb +++ b/lib/gitlab/backend/gitolite_config.rb @@ -14,7 +14,10 @@ module Gitlab end def ga_repo - @ga_repo ||= ::Gitolite::GitoliteAdmin.new(File.join(config_tmp_dir,'gitolite')) + @ga_repo ||= ::Gitolite::GitoliteAdmin.new( + File.join(config_tmp_dir,'gitolite'), + conf: Gitlab.config.gitolite_config_file + ) end def apply diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 43a75cc377b..dd5a9becafc 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -1,10 +1,11 @@ module Grack class Auth < Rack::Auth::Basic + attr_accessor :user, :project def valid? # Authentication with username and password email, password = @auth.credentials - user = User.find_by_email(email) + self.user = User.find_by_email(email) return false unless user.try(:valid_password?, password) # Set GL_USER env variable @@ -17,29 +18,44 @@ module Grack @env['SCRIPT_NAME'] = "" # Find project by PATH_INFO from env - if m = /^\/([\w-]+).git/.match(@request.path_info).to_a - return false unless project = Project.find_by_path(m.last) + if m = /^\/([\w\.-]+)\.git/.match(@request.path_info).to_a + self.project = Project.find_by_path(m.last) + return false unless project end # Git upload and receive if @request.get? - true + validate_get_request elsif @request.post? - if @request.path_info.end_with?('git-upload-pack') - return project.dev_access_for?(user) - elsif @request.path_info.end_with?('git-receive-pack') - if project.protected_branches.map(&:name).include?(current_ref) - project.master_access_for?(user) - else - project.dev_access_for?(user) - end - else - false - end + validate_post_request else false end - end# valid? + end + + def validate_get_request + true + end + + def validate_post_request + if @request.path_info.end_with?('git-upload-pack') + can?(user, :push_code, project) + elsif @request.path_info.end_with?('git-receive-pack') + action = if project.protected_branch?(current_ref) + :push_code_to_protected_branches + else + :push_code + end + + can?(user, action, project) + else + false + end + end + + def can?(object, action, subject) + abilities.allowed?(object, action, subject) + end def current_ref if @env["HTTP_CONTENT_ENCODING"] =~ /gzip/ @@ -49,7 +65,17 @@ module Grack end # Need to reset seek point @request.body.rewind - /refs\/heads\/([\w-]+)/.match(input).to_a.first + /refs\/heads\/([\w\.-]+)/.match(input).to_a.first + end + + protected + + def abilities + @abilities ||= begin + abilities = Six.new + abilities << Ability + abilities + end end end# Auth end# Grack diff --git a/lib/gitlab/file_editor.rb b/lib/gitlab/file_editor.rb deleted file mode 100644 index dc3f9480460..00000000000 --- a/lib/gitlab/file_editor.rb +++ /dev/null @@ -1,58 +0,0 @@ -module Gitlab - # GitLab file editor - # - # It gives you ability to make changes to files - # & commit this changes from GitLab UI. - class FileEditor - attr_accessor :user, :project, :ref - - def initialize(user, project, ref) - self.user = user - self.project = project - self.ref = ref - end - - def update(path, content, commit_message, last_commit) - return false unless can_edit?(path, last_commit) - - Grit::Git.with_timeout(10.seconds) do - lock_file = Rails.root.join("tmp", "#{project.path}.lock") - - File.open(lock_file, "w+") do |f| - f.flock(File::LOCK_EX) - - unless project.satellite.exists? - raise "Satellite doesn't exist" - end - - project.satellite.clear - - Dir.chdir(project.satellite.path) do - r = Grit::Repo.new('.') - r.git.sh "git reset --hard" - r.git.sh "git fetch origin" - r.git.sh "git config user.name \"#{user.name}\"" - r.git.sh "git config user.email \"#{user.email}\"" - r.git.sh "git checkout -b #{ref} origin/#{ref}" - File.open(path, 'w'){|f| f.write(content)} - r.git.sh "git add ." - r.git.sh "git commit -am '#{commit_message}'" - output = r.git.sh "git push origin #{ref}" - - if output =~ /reject/ - return false - end - end - end - end - true - end - - protected - - def can_edit?(path, last_commit) - current_last_commit = @project.last_commit_for(ref, path).sha - last_commit == current_last_commit - end - end -end diff --git a/lib/gitlab/graph/commit.rb b/lib/gitlab/graph/commit.rb new file mode 100644 index 00000000000..e26dd4a6ab4 --- /dev/null +++ b/lib/gitlab/graph/commit.rb @@ -0,0 +1,48 @@ +require "grit" + +module Gitlab + module Graph + class Commit + include ActionView::Helpers::TagHelper + + attr_accessor :time, :space, :refs + + def initialize(commit) + @_commit = commit + @time = -1 + @space = 0 + 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] = Gitlab::Encode.utf8(author.name) + h[:time] = time + h[:space] = space + h[:refs] = refs.collect{|r|r.name}.join(" ") unless refs.nil? + h[:id] = sha + h[:date] = date + h[:message] = escape_once(Gitlab::Encode.utf8(message)) + h[:login] = author.email + 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 new file mode 100644 index 00000000000..c2c3fa662a6 --- /dev/null +++ b/lib/gitlab/graph/json_builder.rb @@ -0,0 +1,172 @@ +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 + @project = project + @repo = project.repo + @ref_cache = {} + + @commits = collect_commits + @days = index_commits + end + + def days_json + @days_json = @days.compact.map { |d| [d.day, d.strftime("%b")] }.to_json + end + + def commits_json + @commits_json = @commits.map(&:to_graph_hash).to_json + end + + protected + + # Get commits from repository + # + def collect_commits + @commits = Grit::Commit.find_all(repo, nil, {max_count: self.class.max_count}).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>] comits to index + # + # @return [Array<TimeDate>] list of commit dates corelated with time on commits + def index_commits + days, heads = [], [] + map = {} + + commits.reverse.each_with_index do |c,i| + c.time = i + days[i] = c.committed_date + map[c.id] = c + heads += c.refs unless c.refs.nil? + end + + heads.select!{|h| h.is_a? Grit::Head or h.is_a? Grit::Remote} + # sort heads so the master is top and current branches are closer + heads.sort! do |a,b| + if a.name == "master" + -1 + elsif b.name == "master" + 1 + else + b.commit.committed_date <=> a.commit.committed_date + end + end + + @_reserved = {} + days.each_index do |i| + @_reserved[i] = [] + end + + heads.each do |h| + if map.include? h.commit.id then + place_chain(map[h.commit.id], map) + end + end + + days + 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 + space = find_free_space(leaves.last.time..leaves.first.time) + leaves.each{|l| l.space = space} + # and mark it as reserved + min_time = leaves.last.time + 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 + end + end + if parent_time.nil? + max_time = leaves.first.time + else + max_time = parent_time - 1 + end + 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 == 0} + 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) + reserved = [] + for day in time_range + reserved += @_reserved[day] + end + space = 1 + while reserved.include? space do + space += 1 + 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 + parent = commit.parents.collect.select do |p| + map.include? p.id and map[p.id].space == 0 + end + + return leaves if parent.count.zero? + + commit = map[parent.first.id] + leaves.push(commit) + end + end + end + end +end diff --git a/lib/gitlab/graph_commit.rb b/lib/gitlab/graph_commit.rb deleted file mode 100644 index d3668a99107..00000000000 --- a/lib/gitlab/graph_commit.rb +++ /dev/null @@ -1,185 +0,0 @@ -require "grit" - -module Gitlab - class GraphCommit - attr_accessor :time, :space - attr_accessor :refs - - include ActionView::Helpers::TagHelper - - def self.to_graph(project) - @repo = project.repo - commits = Grit::Commit.find_all(@repo, nil, {max_count: 650}) - - ref_cache = {} - - commits.map! {|c| GraphCommit.new(Commit.new(c))} - commits.each { |commit| commit.add_refs(ref_cache, @repo) } - - days = GraphCommit.index_commits(commits) - @days_json = days.compact.collect{|d| [d.day, d.strftime("%b")] }.to_json - @commits_json = commits.map(&:to_graph_hash).to_json - - return @days_json, @commits_json - 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<GraphCommit>] comits to index - # - # @return [Array<TimeDate>] list of commit dates corelated with time on commits - def self.index_commits(commits) - days, heads = [], [] - map = {} - - commits.reverse.each_with_index do |c,i| - c.time = i - days[i] = c.committed_date - map[c.id] = c - heads += c.refs unless c.refs.nil? - end - - heads.select!{|h| h.is_a? Grit::Head or h.is_a? Grit::Remote} - # sort heads so the master is top and current branches are closer - heads.sort! do |a,b| - if a.name == "master" - -1 - elsif b.name == "master" - 1 - else - b.commit.committed_date <=> a.commit.committed_date - end - end - - @_reserved = {} - days.each_index do |i| - @_reserved[i] = [] - end - - heads.each do |h| - if map.include? h.commit.id then - place_chain(map[h.commit.id], map) - end - end - days - end - - # Add space mark on commit and its parents - # - # @param [GraphCommit] the commit object. - # @param [Hash<String,GraphCommit>] map of commits - def self.place_chain(commit, map, parent_time = nil) - leaves = take_left_leaves(commit, map) - if leaves.empty? then - return - end - space = find_free_space(leaves.last.time..leaves.first.time) - leaves.each{|l| l.space = space} - # and mark it as reserved - min_time = leaves.last.time - parents = leaves.last.parents.collect - parents.each do |p| - if map.include? p.id then - parent = map[p.id] - if parent.time < min_time then - min_time = parent.time - end - end - end - if parent_time.nil? then - max_time = leaves.first.time - else - max_time = parent_time - 1 - end - 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 == 0} - for p in parents - place_chain(map[p.id], map, l.time) - end - end - end - - def self.mark_reserved(time_range, space) - for day in time_range - @_reserved[day].push(space) - end - end - - def self.find_free_space(time_range) - reserved = [] - for day in time_range - reserved += @_reserved[day] - end - space = 1 - while reserved.include? space do - space += 1 - end - space - end - - # Takes most left subtree branch of commits - # which don't have space mark yet. - # - # @param [GraphCommit] the commit object. - # @param [Hash<String,GraphCommit>] map of commits - # - # @return [Array<GraphCommit>] list of branch commits - def self.take_left_leaves(commit, map) - leaves = [] - leaves.push(commit) if commit.space == 0 - while true - parent = commit.parents.collect - .select{|p| map.include? p.id and map[p.id].space == 0} - if parent.count == 0 then - return leaves - else - commit = map[parent.first.id] - leaves.push(commit) - end - end - end - - - def initialize(commit) - @_commit = commit - @time = -1 - @space = 0 - 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] = Gitlab::Encode.utf8(author.name) - h[:time] = time - h[:space] = space - h[:refs] = refs.collect{|r|r.name}.join(" ") unless refs.nil? - h[:id] = sha - h[:date] = date - h[:message] = escape_once(Gitlab::Encode.utf8(message)) - h[:login] = author.email - 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 diff --git a/lib/gitlab/merge.rb b/lib/gitlab/merge.rb deleted file mode 100644 index 79f86fad37a..00000000000 --- a/lib/gitlab/merge.rb +++ /dev/null @@ -1,65 +0,0 @@ -module Gitlab - class Merge - attr_accessor :project, :merge_request, :user - - def initialize(merge_request, user) - self.user = user - self.merge_request = merge_request - self.project = merge_request.project - end - - def can_be_merged? - result = false - process do |repo, output| - result = !(output =~ /CONFLICT/) - end - result - end - - def merge - process do |repo, output| - if output =~ /CONFLICT/ - false - else - !!repo.git.push({}, "origin", merge_request.target_branch) - end - end - end - - def process - Grit::Git.with_timeout(30.seconds) do - lock_file = Rails.root.join("tmp", "#{project.path}.lock") - - File.open(lock_file, "w+") do |f| - f.flock(File::LOCK_EX) - - unless project.satellite.exists? - raise "Satellite doesn't exist" - end - - project.satellite.clear - - Dir.chdir(project.satellite.path) do - merge_repo = Grit::Repo.new('.') - merge_repo.git.sh "git reset --hard" - merge_repo.git.sh "git fetch origin" - merge_repo.git.sh "git config user.name \"#{user.name}\"" - merge_repo.git.sh "git config user.email \"#{user.email}\"" - merge_repo.git.sh "git checkout -b #{merge_request.target_branch} origin/#{merge_request.target_branch}" - output = merge_repo.git.pull({}, "--no-ff", "origin", merge_request.source_branch) - - #remove source-branch - if merge_request.should_remove_source_branch && !project.root_ref?(merge_request.source_branch) - merge_repo.git.sh "git push origin :#{merge_request.source_branch}" - end - - yield(merge_repo, output) - end - end - end - - rescue Grit::Git::GitTimeout - return false - end - end -end diff --git a/lib/gitlab/satellite.rb b/lib/gitlab/satellite.rb deleted file mode 100644 index 9d8dfb8e0a4..00000000000 --- a/lib/gitlab/satellite.rb +++ /dev/null @@ -1,41 +0,0 @@ -module Gitlab - class Satellite - - PARKING_BRANCH = "__parking_branch" - - attr_accessor :project - - def initialize project - self.project = project - end - - def create - `git clone #{project.url_to_repo} #{path}` - end - - def path - Rails.root.join("tmp", "repo_satellites", project.path) - end - - def exists? - File.exists? path - end - - #will be deleted all branches except PARKING_BRANCH - def clear - Dir.chdir(path) do - heads = Grit::Repo.new(".").heads.map{|head| head.name} - if heads.include? PARKING_BRANCH - `git checkout #{PARKING_BRANCH}` - else - `git checkout -b #{PARKING_BRANCH}` - end - heads.delete(PARKING_BRANCH) - heads.each do |head| - `git branch -D #{head}` - end - end - end - - end -end diff --git a/lib/gitlab/satellite/action.rb b/lib/gitlab/satellite/action.rb new file mode 100644 index 00000000000..ed2541f3998 --- /dev/null +++ b/lib/gitlab/satellite/action.rb @@ -0,0 +1,46 @@ +module Gitlab + module Satellite + class Action + DEFAULT_OPTIONS = { git_timeout: 30.seconds } + + attr_accessor :options, :project, :user + + def initialize(user, project, options = {}) + @options = DEFAULT_OPTIONS.merge(options) + @project = project + @user = user + end + + protected + + # * Sets a 30s timeout for Git + # * Locks the satellite repo + # * Yields the prepared satellite repo + def in_locked_and_timed_satellite + Grit::Git.with_timeout(options[:git_timeout]) do + project.satellite.lock do + return yield project.satellite.repo + end + end + rescue Errno::ENOMEM => ex + Gitlab::GitLogger.error(ex.message) + return false + rescue Grit::Git::GitTimeout => ex + Gitlab::GitLogger.error(ex.message) + return false + end + + # * Clears the satellite + # * Updates the satellite from Gitolite + # * 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) + end + end + end +end diff --git a/lib/gitlab/satellite/edit_file_action.rb b/lib/gitlab/satellite/edit_file_action.rb new file mode 100644 index 00000000000..336afc881c2 --- /dev/null +++ b/lib/gitlab/satellite/edit_file_action.rb @@ -0,0 +1,57 @@ +module Gitlab + module Satellite + # GitLab server-side file update and commit + class EditFileAction < Action + attr_accessor :file_path, :ref + + def initialize(user, project, ref, file_path) + super user, project, git_timeout: 10.seconds + @file_path = file_path + @ref = ref + end + + # 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 pushing from the satellite to Gitolite failed or was rejected + # Returns true otherwise + def commit!(content, commit_message, last_commit) + return false unless can_edit?(last_commit) + + in_locked_and_timed_satellite do |repo| + prepare_satellite!(repo) + + # create target branch in satellite at the corresponding commit from Gitolite + repo.git.checkout({raise: true, timeout: true, b: true}, ref, "origin/#{ref}") + + # update the file in the satellite's working dir + file_path_in_satellite = File.join(repo.working_dir, file_path) + File.open(file_path_in_satellite, 'w') { |f| f.write(content) } + + # commit the changes + # will raise CommandFailed when commit fails + repo.git.commit(raise: true, timeout: true, a: true, m: commit_message) + + + # push commit back to Gitolite + # will raise CommandFailed when push fails + repo.git.push({raise: true, timeout: true}, :origin, ref) + + # everything worked + true + end + rescue Grit::Git::CommandFailed => ex + Gitlab::GitLogger.error(ex.message) + false + end + + protected + + def can_edit?(last_commit) + current_last_commit = @project.last_commit_for(ref, file_path).sha + last_commit == current_last_commit + end + end + end +end diff --git a/lib/gitlab/satellite/merge_action.rb b/lib/gitlab/satellite/merge_action.rb new file mode 100644 index 00000000000..832db6621c4 --- /dev/null +++ b/lib/gitlab/satellite/merge_action.rb @@ -0,0 +1,71 @@ +module Gitlab + module Satellite + # GitLab server-side merge + class MergeAction < Action + attr_accessor :merge_request + + def initialize(user, merge_request) + super user, merge_request.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| + 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. + # + # Returns false if the merge produced conflicts + # Returns false if pushing from the satellite to Gitolite failed or was rejected + # Returns true otherwise + def merge! + in_locked_and_timed_satellite do |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) + + # 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}") + end + + # merge, push and branch removal successful + true + end + end + rescue Grit::Git::CommandFailed => ex + Gitlab::GitLogger.error(ex.message) + false + end + + private + + # Merges the source_branch into the target_branch in the satellite. + # + # Note: it will clear out the satellite before doing anything + # + # 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}") + + # merge the source branch from Gitolite into the satellite + # will raise CommandFailed when merge fails + repo.git.pull({raise: true, timeout: true, no_ff: true}, :origin, merge_request.source_branch) + rescue Grit::Git::CommandFailed => ex + Gitlab::GitLogger.error(ex.message) + false + end + end + end +end diff --git a/lib/gitlab/satellite/satellite.rb b/lib/gitlab/satellite/satellite.rb new file mode 100644 index 00000000000..28b6f538d00 --- /dev/null +++ b/lib/gitlab/satellite/satellite.rb @@ -0,0 +1,92 @@ +module Gitlab + module Satellite + class Satellite + PARKING_BRANCH = "__parking_branch" + + attr_accessor :project + + def initialize(project) + @project = project + end + + def clear_and_update! + raise "Satellite doesn't exist" unless exists? + + delete_heads! + clear_working_dir! + update_from_source! + end + + def create + `git clone #{project.url_to_repo} #{path}` + end + + def exists? + File.exists? path + end + + # * Locks the satellite + # * Changes the current directory to the satellite's working dir + # * Yields + def lock + raise "Satellite doesn't exist" unless exists? + + File.open(lock_file, "w+") do |f| + f.flock(File::LOCK_EX) + + Dir.chdir(path) do + return yield + end + end + end + + def lock_file + Rails.root.join("tmp", "#{project.path}.lock") + end + + def path + Rails.root.join("tmp", "repo_satellites", project.path) + end + + def repo + raise "Satellite doesn't exist" unless exists? + + @repo ||= Grit::Repo.new(path) + end + + private + + # Clear the working directory + def clear_working_dir! + repo.git.reset(hard: true) + end + + # Deletes all branches except the parking branch + # + # This ensures we have no name clashes or issues updating branches when + # working with the satellite. + def delete_heads! + heads = repo.heads.map(&:name) + + # update or create the parking branch + if heads.include? PARKING_BRANCH + repo.git.checkout({}, PARKING_BRANCH) + else + repo.git.checkout({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) } + 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) + end + end + end +end diff --git a/lib/tasks/bulk_import.rake b/lib/tasks/bulk_import.rake deleted file mode 100644 index 914f920a8d7..00000000000 --- a/lib/tasks/bulk_import.rake +++ /dev/null @@ -1,83 +0,0 @@ -desc "Imports existing Git repos from a directory into new projects in git_base_path" -task :import_projects, [:directory,:email] => :environment do |t, args| - user_email, import_directory = args.email, args.directory - repos_to_import = Dir.glob("#{import_directory}/*") - git_base_path = Gitlab.config.git_base_path - imported_count, skipped_count, failed_count = 0 - - puts "Found #{repos_to_import.size} repos to import" - - repos_to_import.each do |repo_path| - repo_name = File.basename repo_path - clone_path = "#{git_base_path}#{repo_name}.git" - - puts " Processing #{repo_name}" - - if Dir.exists? clone_path - if Project.find_by_code(repo_name) - puts " INFO: #{clone_path} already exists in repositories directory, skipping." - skipped_count += 1 - next - else - puts " INFO: Project doesn't exist for #{repo_name} (but the repo does)." - end - else - # Clone the repo - unless clone_bare_repo_as_git(repo_path, clone_path) - failed_count += 1 - next - end - end - - # Create the project and repo - if create_repo_project(repo_name, user_email) - imported_count += 1 - else - failed_count += 1 - end - end - - puts "Finished importing #{imported_count} projects (skipped #{skipped_count}, failed #{failed_count})." -end - -# Clones a repo as bare git repo using the git_user -def clone_bare_repo_as_git(existing_path, new_path) - git_user = Gitlab.config.ssh_user - begin - sh "sudo -u #{git_user} -i git clone --bare '#{existing_path}' #{new_path}" - rescue Exception => msg - puts " ERROR: Failed to clone #{existing_path} to #{new_path}" - puts " Make sure #{git_user} can reach #{existing_path}" - puts " Exception-MSG: #{msg}" - end -end - -# Creates a project in GitLab given a `project_name` to use -# (for name, web url, and code url) and a `user_email` that will be -# assigned as the owner of the project. -def create_repo_project(project_name, user_email) - if user = User.find_by_email(user_email) - # Using find_by_code since that's the most important identifer to be unique - if Project.find_by_code(project_name) - puts " INFO: Project #{project_name} already exists in Gitlab, skipping." - else - project = Project.create( - name: project_name, - code: project_name, - path: project_name, - owner: user, - description: "Automatically created from 'import_projects' rake task on #{Time.now}" - ) - - if project.valid? - # Add user as admin for project - project.users_projects.create!(:project_access => UsersProject::MASTER, :user => user) - project.update_repository - else - puts " ERROR: Failed to create project #{project} because #{project.errors.first}" - end - end - else - puts " ERROR: user with #{user_email} not found, skipping" - end -end diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake new file mode 100644 index 00000000000..09f0dc9e459 --- /dev/null +++ b/lib/tasks/gitlab/import.rake @@ -0,0 +1,54 @@ +namespace :gitlab do + namespace :import do + # How to use: + # + # 1. copy your bare repos under git base_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 + + git_base_path = Gitlab.config.git_base_path + repos_to_import = Dir.glob(git_base_path + '/*') + + repos_to_import.each do |repo_path| + repo_name = File.basename repo_path + + # skip gitolite admin + next if repo_name == 'gitolite-admin.git' + + path = repo_name.sub(/\.git$/, '') + + project = Project.find_by_path(path) + + puts "Processing #{repo_name}".yellow + + if project + puts " * #{project.name} (#{repo_name}) exists" + else + user = User.admins.first + + project_params = { + :name => path, + :code => path, + :path => path, + } + + project = Project.create_by_user(project_params, user) + + if project.valid? + puts " * Created #{project.name} (#{repo_name})".green + else + puts " * Failed trying to create #{project.name} (#{repo_name})".red + end + end + end + + puts "Done!".green + end + end +end diff --git a/lib/tasks/gitlab/status.rake b/lib/tasks/gitlab/status.rake index 302f417c01a..3878823cc94 100644 --- a/lib/tasks/gitlab/status.rake +++ b/lib/tasks/gitlab/status.rake @@ -49,7 +49,7 @@ namespace :gitlab do end print "UMASK for .gitolite.rc is 0007? ............" - if open("#{git_base_path}/../.gitolite.rc").grep(/UMASK([ \t]*)=([ \t>]*)0007/).any? + if open(File.absolute_path("#{git_base_path}/../.gitolite.rc")).grep(/UMASK([ \t]*)=([ \t>]*)0007/).any? puts "YES".green else puts "NO".red |