Commit bb6767e5 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'gitaly-worktree-refactor' into 'master'

Move git operations for squash and rebase into Gitlab::Git

Closes gitaly#761

See merge request gitlab-org/gitlab-ee!3543
parents f6793a80 57933b9f
...@@ -16,40 +16,18 @@ module EE ...@@ -16,40 +16,18 @@ module EE
delegate :sha, to: :base_pipeline, prefix: :base_pipeline, allow_nil: true delegate :sha, to: :base_pipeline, prefix: :base_pipeline, allow_nil: true
end end
def rebase_dir_path
File.join(::Gitlab.config.shared.path, 'tmp/rebase', source_project.id.to_s, id.to_s).to_s
end
def squash_dir_path
File.join(::Gitlab.config.shared.path, 'tmp/squash', source_project.id.to_s, id.to_s).to_s
end
def rebase_in_progress? def rebase_in_progress?
# The source project can be deleted # The source project can be deleted
return false unless source_project return false unless source_project
File.exist?(rebase_dir_path) && !clean_stuck_rebase source_project.repository.rebase_in_progress?(id)
end end
def squash_in_progress? def squash_in_progress?
# The source project can be deleted # The source project can be deleted
return false unless source_project return false unless source_project
File.exist?(squash_dir_path) && !clean_stuck_squash source_project.repository.squash_in_progress?(id)
end
def clean_stuck_rebase
if File.mtime(rebase_dir_path) < 15.minutes.ago
FileUtils.rm_rf(rebase_dir_path)
true
end
end
def clean_stuck_squash
if File.mtime(squash_dir_path) < 15.minutes.ago
FileUtils.rm_rf(squash_dir_path)
true
end
end end
def squash def squash
......
...@@ -38,5 +38,20 @@ module EE ...@@ -38,5 +38,20 @@ module EE
def delete_remote_branches(remote, branches) def delete_remote_branches(remote, branches)
gitlab_shell.delete_remote_branches(repository_storage_path, disk_path, remote, branches) gitlab_shell.delete_remote_branches(repository_storage_path, disk_path, remote, branches)
end end
def rebase(user, merge_request)
raw.rebase(user, merge_request.id, branch: merge_request.source_branch,
branch_sha: merge_request.source_branch_sha,
remote_repository: merge_request.target_project.repository.raw,
remote_branch: merge_request.target_branch)
end
def squash(user, merge_request)
raw.squash(user, merge_request.id, branch: merge_request.target_branch,
start_sha: merge_request.diff_start_sha,
end_sha: merge_request.diff_head_sha,
author: merge_request.author,
message: merge_request.title)
end
end end
end end
...@@ -16,48 +16,15 @@ module MergeRequests ...@@ -16,48 +16,15 @@ module MergeRequests
return false return false
end end
run_git_command( rebase_sha = repository.rebase(current_user, merge_request)
%W(worktree add --detach #{tree_path} #{merge_request.source_branch}),
repository.path_to_repo,
git_env,
'add worktree for rebase'
)
run_git_command(
%W(pull --rebase #{target_project.repository.path_to_repo} #{merge_request.target_branch}),
tree_path,
git_env.merge('GIT_COMMITTER_NAME' => current_user.name,
'GIT_COMMITTER_EMAIL' => current_user.email),
'rebase branch'
)
rebase_sha = run_git_command(
%w(rev-parse HEAD),
tree_path,
git_env,
'get SHA of rebased branch'
)
Gitlab::Git::OperationService.new(current_user, project.repository.raw_repository)
.update_branch(merge_request.source_branch, rebase_sha, merge_request.source_branch_sha)
merge_request.update_attributes(rebase_commit_sha: rebase_sha) merge_request.update_attributes(rebase_commit_sha: rebase_sha)
true true
rescue GitCommandError
false
rescue => e rescue => e
log_error('Failed to rebase branch:') log_error('Failed to rebase branch:')
log_error(e.message, save_message_on_model: true) log_error(e.message, save_message_on_model: true)
false false
ensure
clean_dir
end
private
def tree_path
@tree_path ||= merge_request.rebase_dir_path
end end
end end
end end
...@@ -2,12 +2,9 @@ require 'securerandom' ...@@ -2,12 +2,9 @@ require 'securerandom'
module MergeRequests module MergeRequests
class SquashService < MergeRequests::WorkingCopyBaseService class SquashService < MergeRequests::WorkingCopyBaseService
attr_reader :repository, :rugged
def execute(merge_request) def execute(merge_request)
@merge_request = merge_request @merge_request = merge_request
@repository = target_project.repository @repository = target_project.repository
@rugged = repository.rugged
squash || error('Failed to squash. Should be done manually.') squash || error('Failed to squash. Should be done manually.')
end end
...@@ -25,113 +22,13 @@ module MergeRequests ...@@ -25,113 +22,13 @@ module MergeRequests
return error('Squash task canceled: another squash is already in progress.') return error('Squash task canceled: another squash is already in progress.')
end end
run_git_command( squash_sha = repository.squash(current_user, merge_request)
%W(worktree add --no-checkout --detach #{tree_path}),
repository.path_to_repo,
git_env,
'add worktree for squash'
)
configure_sparse_checkout
diff = git_command(%W(diff --binary #{diff_range}))
apply = git_command(%w(apply --index))
run_command(
["#{diff.join(' ')} | #{apply.join(' ')}"],
tree_path,
git_env,
'apply patch'
)
run_git_command(
%W(commit --no-verify --message #{merge_request.title}),
tree_path,
git_env.merge('GIT_COMMITTER_NAME' => current_user.name,
'GIT_COMMITTER_EMAIL' => current_user.email,
'GIT_AUTHOR_NAME' => merge_request.author.name,
'GIT_AUTHOR_EMAIL' => merge_request.author.email),
'commit squashed changes'
)
# May print a warning for ambiguous refs, but we can ignore that with
# `--quiet` and just take the SHA, if present. HEAD here always refers to
# the current HEAD commit, even if there is another ref called HEAD.
#
squash_sha = run_git_command(
%w(rev-parse --quiet --verify HEAD),
tree_path,
git_env,
'get SHA of squashed commit'
)
success(squash_sha: squash_sha) success(squash_sha: squash_sha)
rescue GitCommandError
false
rescue => e rescue => e
log_error("Failed to squash merge request #{merge_request.to_reference(full: true)}:") log_error("Failed to squash merge request #{merge_request.to_reference(full: true)}:")
log_error(e.message) log_error(e.message)
false false
ensure
clean_dir
clean_worktree
end
def tree_path
@tree_path ||= merge_request.squash_dir_path
end
def diff_range
@diff_range ||= "#{merge_request.diff_start_sha}...#{merge_request.diff_head_sha}"
end
def worktree_path
@worktree_path ||= File.join(repository.path_to_repo, 'worktrees', merge_request.id.to_s)
end
def clean_worktree
FileUtils.rm_rf(worktree_path) if File.exist?(worktree_path)
end
# Adding a worktree means checking out the repository. For large repos, this
# can be very expensive, so set up sparse checkout for the worktree to only
# check out the files we're interested in.
#
def configure_sparse_checkout
run_git_command(
%w(config core.sparseCheckout true),
repository.path_to_repo,
git_env,
'configure sparse checkout'
)
# Get the same diff we'll apply, excluding added files. (We can't check
# out files on the target branch if they don't exist yet!)
#
diff_files = run_git_command(
%W(diff --name-only --diff-filter=a --binary #{diff_range}),
repository.path_to_repo,
git_env,
'get files in diff'
)
# If only new files are introduced by this MR, then our sparse checkout
# doesn't need to have any files at all.
#
unless diff_files.empty?
worktree_info = File.join(worktree_path, 'info')
FileUtils.mkdir_p(worktree_info) unless File.directory?(worktree_info)
File.write(File.join(worktree_info, 'sparse-checkout'), diff_files)
end
run_git_command(
%W(checkout --detach #{merge_request.target_branch}),
tree_path,
git_env,
'check out target branch'
)
end end
end end
end end
module MergeRequests module MergeRequests
class WorkingCopyBaseService < MergeRequests::BaseService class WorkingCopyBaseService < MergeRequests::BaseService
GitCommandError = Class.new(StandardError)
include Gitlab::Popen
attr_reader :merge_request attr_reader :merge_request
def git_command(command)
[Gitlab.config.git.bin_path] + command
end
def run_git_command(command, path, env, message = nil, &block)
run_command(git_command(command), path, env, message, &block)
end
def run_command(command, path, env, message = nil, &block)
output, status = popen(command, path, env, &block)
unless status.zero?
if message
log_error("Failed to #{message} with `#{command.join(' ')}`:")
else
log_error("`#{command.join(' ')}` failed:")
end
log_error(output, save_message_on_model: true)
raise GitCommandError
end
output.chomp
end
def source_project def source_project
@source_project ||= merge_request.source_project @source_project ||= merge_request.source_project
end end
...@@ -46,18 +16,6 @@ module MergeRequests ...@@ -46,18 +16,6 @@ module MergeRequests
merge_request.update(merge_error: message) if save_message_on_model merge_request.update(merge_error: message) if save_message_on_model
end end
def clean_dir
FileUtils.rm_rf(tree_path) if File.exist?(tree_path)
end
def git_env
{
'GL_ID' => Gitlab::GlId.gl_id(current_user),
'GL_PROTOCOL' => 'web',
'GL_REPOSITORY' => Gitlab::GlRepository.gl_repository(project, false)
}
end
# Don't try to print expensive instance variables. # Don't try to print expensive instance variables.
def inspect def inspect
"#<#{self.class} #{merge_request.to_reference(full: true)}>" "#<#{self.class} #{merge_request.to_reference(full: true)}>"
......
...@@ -51,7 +51,7 @@ describe MergeRequest do ...@@ -51,7 +51,7 @@ describe MergeRequest do
end end
it 'returns false when there is no rebase directory' do it 'returns false when there is no rebase directory' do
allow(File).to receive(:exist?).with(subject.rebase_dir_path).and_return(false) allow(File).to receive(:exist?).and_return(false)
expect(subject.rebase_in_progress?).to be_falsey expect(subject.rebase_in_progress?).to be_falsey
end end
...@@ -87,7 +87,7 @@ describe MergeRequest do ...@@ -87,7 +87,7 @@ describe MergeRequest do
end end
it 'returns false when there is no squash directory' do it 'returns false when there is no squash directory' do
allow(File).to receive(:exist?).with(subject.squash_dir_path).and_return(false) allow(File).to receive(:exist?).and_return(false)
expect(subject.squash_in_progress?).to be_falsey expect(subject.squash_in_progress?).to be_falsey
end end
......
...@@ -10,6 +10,7 @@ describe MergeRequests::RebaseService do ...@@ -10,6 +10,7 @@ describe MergeRequests::RebaseService do
target_branch: 'master') target_branch: 'master')
end end
let(:project) { merge_request.project } let(:project) { merge_request.project }
let(:repository) { project.repository.raw }
subject(:service) { described_class.new(project, user, {}) } subject(:service) { described_class.new(project, user, {}) }
...@@ -37,7 +38,7 @@ describe MergeRequests::RebaseService do ...@@ -37,7 +38,7 @@ describe MergeRequests::RebaseService do
context 'when unexpected error occurs' do context 'when unexpected error occurs' do
before do before do
allow(service).to receive(:run_git_command).and_raise('Something went wrong') allow(repository).to receive(:run_git!).and_raise('Something went wrong')
end end
it 'saves the error message' do it 'saves the error message' do
...@@ -54,7 +55,7 @@ describe MergeRequests::RebaseService do ...@@ -54,7 +55,7 @@ describe MergeRequests::RebaseService do
context 'with git command failure' do context 'with git command failure' do
before do before do
allow(service).to receive(:popen).and_return(['Something went wrong', 1]) allow(repository).to receive(:run_git!).and_raise(Gitlab::Git::Repository::GitError, 'Something went wrong')
end end
it 'saves the error message' do it 'saves the error message' do
...@@ -96,12 +97,9 @@ describe MergeRequests::RebaseService do ...@@ -96,12 +97,9 @@ describe MergeRequests::RebaseService do
context 'git commands' do context 'git commands' do
it 'sets GL_REPOSITORY env variable when calling git commands' do it 'sets GL_REPOSITORY env variable when calling git commands' do
expect_any_instance_of(described_class) expect(repository).to receive(:popen).exactly(3)
.to receive(:run_git_command).exactly(3).with( .with(anything, anything, hash_including('GL_REPOSITORY'))
anything, .and_return(['', 0])
anything,
hash_including('GL_REPOSITORY'),
anything)
service.execute(merge_request) service.execute(merge_request)
end end
......
...@@ -4,7 +4,11 @@ describe MergeRequests::SquashService do ...@@ -4,7 +4,11 @@ describe MergeRequests::SquashService do
let(:service) { described_class.new(project, user, {}) } let(:service) { described_class.new(project, user, {}) }
let(:user) { project.owner } let(:user) { project.owner }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw }
let(:log_error) { "Failed to squash merge request #{merge_request.to_reference(full: true)}:" }
let(:squash_dir_path) do
File.join(Gitlab.config.shared.path, 'tmp/squash', repository.gl_repository, merge_request.id.to_s)
end
let(:merge_request_with_one_commit) do let(:merge_request_with_one_commit) do
create(:merge_request, create(:merge_request,
source_branch: 'feature', source_project: project, source_branch: 'feature', source_project: project,
...@@ -32,9 +36,9 @@ describe MergeRequests::SquashService do ...@@ -32,9 +36,9 @@ describe MergeRequests::SquashService do
end end
it 'cleans up the temporary directory' do it 'cleans up the temporary directory' do
expect(service).to receive(:clean_dir).and_call_original
service.execute(merge_request) service.execute(merge_request)
expect(File.exist?(squash_dir_path)).to be(false)
end end
it 'does not keep the branch push event' do it 'does not keep the branch push event' do
...@@ -78,7 +82,7 @@ describe MergeRequests::SquashService do ...@@ -78,7 +82,7 @@ describe MergeRequests::SquashService do
end end
it 'does not perform any git actions' do it 'does not perform any git actions' do
expect(service).not_to receive(:run_git_command) expect(repository).not_to receive(:popen)
service.execute(merge_request_with_one_commit) service.execute(merge_request_with_one_commit)
end end
...@@ -128,13 +132,13 @@ describe MergeRequests::SquashService do ...@@ -128,13 +132,13 @@ describe MergeRequests::SquashService do
a_collection_starting_with([Gitlab.config.git.bin_path] + command.split) a_collection_starting_with([Gitlab.config.git.bin_path] + command.split)
) )
allow(service).to receive(:popen).and_return(['', 0]) allow(repository).to receive(:popen).and_return(['', 0])
allow(service).to receive(:popen).with(git_command, anything, anything).and_return([error, 1]) allow(repository).to receive(:popen).with(git_command, anything, anything).and_return([error, 1])
end end
it 'logs the stage and output' do it 'logs the stage and output' do
expect(service).to receive(:log_error).with(a_string_including(stage)) expect(service).to receive(:log_error).with(log_error)
expect(service).to receive(:log_error).with(error, save_message_on_model: true) expect(service).to receive(:log_error).with(error)
service.execute(merge_request) service.execute(merge_request)
end end
...@@ -145,7 +149,7 @@ describe MergeRequests::SquashService do ...@@ -145,7 +149,7 @@ describe MergeRequests::SquashService do
end end
it 'cleans up the temporary directory' do it 'cleans up the temporary directory' do
expect(service).to receive(:clean_dir).and_call_original expect(File.exist?(squash_dir_path)).to be(false)
service.execute(merge_request) service.execute(merge_request)
end end
...@@ -173,9 +177,9 @@ describe MergeRequests::SquashService do ...@@ -173,9 +177,9 @@ describe MergeRequests::SquashService do
end end
it 'cleans up the temporary directory' do it 'cleans up the temporary directory' do
expect(service).to receive(:clean_dir).and_call_original
service.execute(merge_request) service.execute(merge_request)
expect(File.exist?(squash_dir_path)).to be(false)
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment