diff --git a/back/app/services/github.rb b/back/app/services/github.rb index 8715b0f..cc4a6e5 100644 --- a/back/app/services/github.rb +++ b/back/app/services/github.rb @@ -46,47 +46,57 @@ def create_repository(name) def get_all_files(repo_name) return [] unless repository_exists?(repo_name) + return [] if repository_empty?(repo_name) + + repository_name = set_repository_name(repo_name) + head_sha = @client.ref(repository_name, 'heads/main').object.sha + + # ディレクトリ再帰巡回の代わりに Trees API で全パスを1回のAPIコールで取得 + tree = @client.tree(repository_name, head_sha, recursive: true) + file_blobs = tree.tree.select { |item| item.type == 'blob' } + + # 各ファイルのコンテンツ取得を並列実行 + futures = file_blobs.map do |blob| + Concurrent::Future.execute do + content = @client.blob(repository_name, blob.sha) + decoded = Base64.decode64(content.content.gsub(/\s+/, '')) + { name: File.basename(blob.path), path: blob.path, content: decoded } + end + end - @remote_files = get_all_files_recursive(repo_name).flatten - @remote_files + @remote_files = futures.map(&:value!) + rescue Octokit::Error => e + Rails.logger.error e + [] end def commit_push(repo_name, files, commitMessage) repository_name = set_repository_name(repo_name) branch = 'main' - begin - # 現在のツリーを取得 - base_tree = @client.ref(repository_name, "heads/#{branch}").object.sha + base_tree = @client.ref(repository_name, "heads/#{branch}").object.sha - blobs = files.map do |file| + # blob作成を並列実行 + futures = files.map do |file| + Concurrent::Future.execute do if file[:is_delete] - # ファイルの削除 delete_file(file[:path]) elsif file[:path] == file[:old_path] || file[:old_path].empty? - # ファイルの新規作成と更新 create_or_update_file(repository_name, file) else - # ファイル名の変更 - # ファイル名変更のメソッドがないため、削除と新規作成を行う - delete_blob = delete_file(file[:old_path]) - create_blob = create_or_update_file(repository_name, file) - [delete_blob, create_blob] + [delete_file(file[:old_path]), create_or_update_file(repository_name, file)] end - end.flatten - - # 新しいツリーを作成 - new_tree = @client.create_tree(repository_name, blobs, base_tree: base_tree) + end + end - # 新しいコミットを作成 - new_commit = @client.create_commit(repository_name, commitMessage, new_tree.sha, base_tree) + blobs = futures.map(&:value!).flatten - # リモートリポジトリにプッシュ - @client.update_ref(repository_name, "heads/#{branch}", new_commit.sha) - rescue Octokit::Error => e - Rails.logger.error e - { success: false, message: 'コミットに失敗しました'} - end + new_tree = @client.create_tree(repository_name, blobs, base_tree: base_tree) + new_commit = @client.create_commit(repository_name, commitMessage, new_tree.sha, base_tree) + @client.update_ref(repository_name, "heads/#{branch}", new_commit.sha) + rescue Octokit::Error => e + Rails.logger.error e + { success: false, message: 'コミットに失敗しました'} end private @@ -101,40 +111,18 @@ def repository_empty?(repo_name) true end - def get_all_files_recursive(repo_name, path = '') - return [] if repository_empty?(repo_name) - contents = @client.contents(set_repository_name(repo_name), path: path) - - files = [] - - contents.each do |content| - if content.type == 'file' - file_contents = @client.contents(set_repository_name(repo_name), path: content.path) - files << { name: content.name, path: content.path, content: Base64.decode64(file_contents.content) } - elsif content.type == 'dir' - files << get_all_files_recursive(repo_name, content.path) - end - end - - files - end - def exist_file_update(repo_name, files) return [] if files.nil? || files.empty? error = [] files.each do |file| begin - # contentがnilだとエラーが発生するので空文字を代入 content = file[:content].presence || '' - - # contentとリモートリポジトリのファイル内容が同じ場合は更新しない next if @remote_files.any? { |f| f[:name] == file[:name] && f[:content] == content } file_contents = @client.contents(set_repository_name(repo_name), path: file[:name]) - sha = file_contents.sha - @client.update_contents(set_repository_name(repo_name), file[:name], "更新 #{@commitMessage}", sha,content) + @client.update_contents(set_repository_name(repo_name), file[:name], "更新 #{@commitMessage}", sha, content) rescue Octokit::Error => e Rails.logger.error e error << "#{file[:name]} の更新に失敗しました" @@ -150,7 +138,7 @@ def new_file_create(repo_name, files) error = [] files.each do |file| begin - content = file[:content] || '' # 空の内容でも許容する + content = file[:content] || '' @client.create_contents(set_repository_name(repo_name), file[:name], "作成 #{@commitMessage}", content) rescue Octokit::Error => e Rails.logger.error e @@ -168,8 +156,6 @@ def file_delete(repo_name, files) files.each do |file| begin file_contents = @client.contents(set_repository_name(repo_name), path: file[:name]) - - # ファイルが存在しない場合はスキップ next if file_contents.nil? sha = file_contents.sha diff --git a/back/spec/services/github_spec.rb b/back/spec/services/github_spec.rb index cfb93f2..de2b4e0 100644 --- a/back/spec/services/github_spec.rb +++ b/back/spec/services/github_spec.rb @@ -56,6 +56,63 @@ end end + describe '#get_all_files' do + let(:ref_double) { double('ref', object: double('object', sha: 'head_sha')) } + let(:blob_item_1) { double('item', type: 'blob', path: 'README.md', sha: 'sha1') } + let(:blob_item_2) { double('item', type: 'blob', path: 'notes/memo.md', sha: 'sha2') } + let(:tree_item_dir) { double('item', type: 'tree', path: 'notes', sha: 'dir_sha') } + let(:tree_double) { double('tree', tree: [blob_item_1, blob_item_2, tree_item_dir]) } + let(:blob1) { double('blob', content: Base64.encode64('# Hello')) } + let(:blob2) { double('blob', content: Base64.encode64('- memo')) } + + before do + allow(octokit_client).to receive(:repository).and_return(double('repo')) + allow(octokit_client).to receive(:contents).and_return([double('content')]) + allow(octokit_client).to receive(:ref).and_return(ref_double) + allow(octokit_client).to receive(:tree).and_return(tree_double) + allow(octokit_client).to receive(:blob).with("#{user.name}/myrepo", 'sha1').and_return(blob1) + allow(octokit_client).to receive(:blob).with("#{user.name}/myrepo", 'sha2').and_return(blob2) + end + + it 'Trees APIを再帰オプションで呼び出す' do + github.get_all_files('myrepo') + expect(octokit_client).to have_received(:tree).with("#{user.name}/myrepo", 'head_sha', recursive: true) + end + + it 'blobのみを対象にファイル一覧を返す' do + result = github.get_all_files('myrepo') + expect(result.size).to eq(2) + expect(result).to include( + { name: 'README.md', path: 'README.md', content: '# Hello' }, + { name: 'memo.md', path: 'notes/memo.md', content: '- memo' } + ) + end + + context 'リポジトリが存在しない場合' do + before { allow(octokit_client).to receive(:repository).and_raise(Octokit::NotFound) } + + it '空配列を返す' do + expect(github.get_all_files('myrepo')).to eq([]) + end + end + + context 'リポジトリが空の場合' do + before { allow(octokit_client).to receive(:contents).and_raise(Octokit::NotFound) } + + it '空配列を返す' do + expect(github.get_all_files('myrepo')).to eq([]) + end + end + + context 'GitHub APIがエラーを返した場合' do + before { allow(octokit_client).to receive(:ref).and_raise(Octokit::Error) } + + it '空配列を返す' do + expect(github.get_all_files('myrepo')).to eq([]) + end + end + end + describe '#commit_push' do let(:ref_double) { double('ref', object: double('object', sha: 'base_sha')) } let(:tree_double) { double('tree', sha: 'tree_sha') }