up:: ruby

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
 
require 'rugged'
require 'logger'
require 'optparse'
 
class GitAutomation
  def initialize(repo_path, config = {})
    @repo_path = File.expand_path(repo_path)
    @config = default_config.merge(config)
    @logger = setup_logger
    
    # Ruggedリポジトリオブジェクトを初期化
    @repo = Rugged::Repository.open(@repo_path)
    @logger.info("リポジトリを開きました: #{@repo_path}")
  rescue Rugged::OSError, Rugged::RepositoryError => e
    @logger.error("リポジトリの初期化に失敗: #{e.message}")
    exit(1)
  end
 
  # デフォルト設定を定義
  def default_config
    {
      commit_message: "自動コミット - #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}",
      remote_name: 'origin',
      branch_name: 'main',
      auto_add_all: true,
      force_push: false
    }
  end
 
  # ログ設定を初期化
  def setup_logger
    logger = Logger.new(STDOUT)
    logger.level = Logger::INFO
    logger.formatter = proc do |severity, datetime, progname, msg|
      "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n"
    end
    logger
  end
 
  # ターミナル起動時のメイン処理
  def run_startup_sequence
    @logger.info("Git自動化処理を開始します")
    
    begin
      # 1. 現在の状態をチェック
      check_repository_status
      
      # 2. 既存の競合状態の確認
      if has_merge_conflicts?
        handle_unresolved_conflicts
        exit(0)
      end
      
      # 3. ローカル変更があればコミット
      commit_changes if has_changes?
      
      # 4. リモートから最新情報を取得(プル操作)
      pull_result = pull_changes if @config[:auto_pull]
      
      # 5. プル時に競合が発生した場合の処理
      if pull_result == :conflicts
        handle_merge_conflicts
        exit(0)
      end
      
      # 6. リモートにプッシュ
      push_changes if @config[:auto_push]
      
      @logger.info("全ての処理が正常に完了しました")
      
    rescue => e
      @logger.error("処理中にエラーが発生: #{e.message}")
      @logger.debug(e.backtrace.join("\n"))
      exit(1)
    end
  end
 
  private
 
  # リポジトリの状態をチェック
  def check_repository_status
    @logger.info("リポジトリの状態をチェックしています...")
    
    # ワーキングディレクトリの状態を取得
    status = @repo.status { |file, flags| 
      @logger.debug("#{file}: #{status_flags_to_string(flags)}")
    }
    
    @logger.info("#{status.count}個のファイルに変更があります")
  end
 
  # ステータスフラグを文字列に変換(デバッグ用)
  def status_flags_to_string(flags)
    status_types = []
    status_types << "新規" if flags.include?(:wt_new)
    status_types << "変更" if flags.include?(:wt_modified)
    status_types << "削除" if flags.include?(:wt_deleted)
    status_types << "ステージング済み" if flags.include?(:index_modified)
    status_types.join(", ")
  end
 
  # 競合状態をチェック
  def has_merge_conflicts?
    # ワーキングディレクトリで競合しているファイルがあるかチェック
    @repo.status.any? { |file, flags| flags.include?(:conflicted) }
  end
 
  # 変更があるかチェック
  def has_changes?
    # ワーキングディレクトリまたはインデックスに変更があるかチェック
    !@repo.status.empty?
  end
 
  # 未解決の競合を処理
  def handle_unresolved_conflicts
    @logger.error("=" * 60)
    @logger.error("未解決のマージ競合が検出されました!")
    @logger.error("=" * 60)
    
    # 競合しているファイルを表示
    conflicted_files = []
    @repo.status.each do |file, flags|
      if flags.include?(:conflicted)
        conflicted_files << file
        @logger.error("競合ファイル: #{file}")
      end
    end
    
    @logger.error("")
    @logger.error("手動で競合を解決してください:")
    @logger.error("1. 上記のファイルで競合マーカー(<<<<<<<, =======, >>>>>>>)を解決")
    @logger.error("2. git add <解決したファイル> でステージング")
    @logger.error("3. git commit でマージコミットを作成")
    @logger.error("4. このスクリプトを再実行してください")
    @logger.error("")
    @logger.error("現在の状態確認: git status")
    @logger.error("=" * 60)
  end
 
  # マージ競合を処理(プル時に発生)
  def handle_merge_conflicts
    @logger.error("=" * 60)
    @logger.error("プル時にマージ競合が発生しました!")
    @logger.error("=" * 60)
    
    # 競合しているファイルを表示
    conflicted_files = []
    @repo.status.each do |file, flags|
      if flags.include?(:conflicted)
        conflicted_files << file
        @logger.error("競合ファイル: #{file}")
      end
    end
    
    @logger.error("")
    @logger.error("リモートの変更とローカルの変更が競合しています。")
    @logger.error("")
    @logger.error("手動で競合を解決してください:")
    @logger.error("1. 上記のファイルで競合マーカーを解決")
    @logger.error("2. git add <解決したファイル> でステージング")
    @logger.error("3. git commit でマージコミットを作成")
    @logger.error("4. このスクリプトを再実行してください")
    @logger.error("")
    @logger.error("現在の状態確認: git status")
    @logger.error("競合解決のヒント: git diff")
    @logger.error("=" * 60)
  end
 
  # リモートから変更を取得(プル操作)
  def pull_changes
    @logger.info("リモートから最新の変更を取得しています...")
    
    # リモートブランチの参照を取得
    remote = @repo.remotes[@config[:remote_name]]
    raise "リモート '#{@config[:remote_name]}' が見つかりません" unless remote
    
    # フェッチを実行
    remote.fetch
    @logger.info("フェッチが完了しました")
    
    # マージ処理を実行し、結果を返す
    perform_merge
  end
 
  # マージ処理を実行
  def perform_merge
    remote_branch = "refs/remotes/#{@config[:remote_name]}/#{@config[:branch_name]}"
    
    begin
      remote_commit = @repo.references[remote_branch].target
      head_commit = @repo.head.target
      
      # マージベースを確認
      merge_base = @repo.merge_base(head_commit, remote_commit)
      
      if merge_base == head_commit.oid
        # Fast-forward可能
        @repo.references.update("refs/heads/#{@config[:branch_name]}", remote_commit.oid)
        @repo.checkout_head(strategy: :force)
        @logger.info("Fast-forwardマージを実行しました")
        :success
      elsif merge_base == remote_commit.oid
        # ローカルブランチの方が進んでいる(リモートに変更なし)
        @logger.info("ローカルブランチの方が進んでいます")
        :success
      else
        # 3-way マージが必要
        @logger.info("3-wayマージを実行しています...")
        
        begin
          # 3-wayマージを実行
          merge_index = @repo.merge_commits(head_commit, remote_commit)
          
          if merge_index.conflicts?
            # 競合が発生
            @logger.warn("マージ競合が検出されました")
            
            # 競合情報をワーキングディレクトリに書き出し
            @repo.checkout_index(merge_index, strategy: :allow_conflicts)
            
            return :conflicts
          else
            # 競合なしでマージ可能
            @repo.index.read(merge_index)
            
            # マージコミットを作成
            signature = get_signature
            merge_commit_oid = Rugged::Commit.create(@repo,
              author: signature,
              committer: signature,
              message: "Merge branch '#{@config[:remote_name]}/#{@config[:branch_name]}'",
              parents: [head_commit, remote_commit],
              tree: @repo.index.write_tree(@repo)
            )
            
            # HEADを更新
            @repo.references.update("refs/heads/#{@config[:branch_name]}", merge_commit_oid)
            
            @logger.info("3-wayマージが完了しました")
            :success
          end
        rescue => e
          @logger.error("マージ処理でエラー: #{e.message}")
          :conflicts
        end
      end
      
    rescue Rugged::ReferenceError => e
      @logger.warn("リモートブランチが見つかりません: #{e.message}")
      :success
    end
  end
 
  # 変更をコミット
  def commit_changes
    @logger.info("変更をコミットしています...")
    
    # 全ファイルをステージングエリアに追加(設定により)
    if @config[:auto_add_all]
      add_all_changes
    end
    
    # ステージングエリアが空でないかチェック
    return unless has_staged_changes?
    
    # コミットを作成
    create_commit
  end
 
  # 全ての変更をステージングエリアに追加
  def add_all_changes
    @logger.debug("全ての変更をステージングエリアに追加しています...")
    
    index = @repo.index
    
    # ワーキングディレクトリの全変更を追加
    @repo.status.each do |file, flags|
      if flags.include?(:wt_new) || flags.include?(:wt_modified)
        index.add(file)
        @logger.debug("追加: #{file}")
      elsif flags.include?(:wt_deleted)
        index.remove(file)
        @logger.debug("削除: #{file}")
      end
    end
    
    index.write
    @logger.info("ステージングが完了しました")
  end
 
  # ステージングエリアに変更があるかチェック
  def has_staged_changes?
    # HEADとインデックスを比較
    head_tree = @repo.head.target.tree
    index_tree = @repo.index.write_tree(@repo)
    
    head_tree.oid != index_tree
  end
 
  # コミットを作成
  def create_commit
    # 署名情報を取得(Gitの設定から)
    signature = get_signature
    
    # 親コミットを取得
    parents = @repo.empty? ? [] : [@repo.head.target]
    
    # コミットを作成
    commit_oid = Rugged::Commit.create(@repo,
      author: signature,
      committer: signature,
      message: @config[:commit_message],
      parents: parents,
      tree: @repo.index.write_tree(@repo)
    )
    
    @logger.info("コミットを作成しました: #{commit_oid[0..7]}")
    @logger.info("メッセージ: #{@config[:commit_message]}")
  end
 
  # Git署名情報を取得
  def get_signature
    config = @repo.config
    name = config['user.name'] || ENV['USER'] || 'Unknown User'
    email = config['user.email'] || "#{ENV['USER']}@localhost"
    
    Rugged::Signature.new(name, email, Time.now)
  end
 
  # リモートにプッシュ
  def push_changes
    @logger.info("リモートにプッシュしています...")
    
    remote = @repo.remotes[@config[:remote_name]]
    raise "リモート '#{@config[:remote_name]}' が見つかりません" unless remote
    
    # プッシュを実行
    refspec = "refs/heads/#{@config[:branch_name]}:refs/heads/#{@config[:branch_name]}"
    options = {}
    options[:force] = true if @config[:force_push]
    
    remote.push([refspec], options)
    @logger.info("プッシュが完了しました")
  rescue Rugged::NetworkError => e
    @logger.error("ネットワークエラー: #{e.message}")
    raise
  end
end
 
# コマンドライン引数の処理
def parse_options
  options = {
    repo_path: Dir.pwd,
    auto_pull: true,
    auto_push: true,
    commit_message: nil
  }
  
  OptionParser.new do |opts|
    opts.banner = "使用法: #{$0} [オプション]"
    
    opts.on("-r", "--repo PATH", "リポジトリのパス(デフォルト: カレントディレクトリ)") do |path|
      options[:repo_path] = path
    end
    
    opts.on("-m", "--message MESSAGE", "コミットメッセージ") do |message|
      options[:commit_message] = message
    end
    
    opts.on("--no-pull", "プル処理をスキップ") do
      options[:auto_pull] = false
    end
    
    opts.on("--no-push", "プッシュ処理をスキップ") do
      options[:auto_push] = false
    end
    
    opts.on("--force-push", "強制プッシュを有効にする") do
      options[:force_push] = true
    end
    
    opts.on("-h", "--help", "このヘルプを表示") do
      puts opts
      exit
    end
  end.parse!
  
  options
end
 
# メイン実行部分
if __FILE__ == $0
  begin
    options = parse_options
    
    # 設定を準備
    config = {
      auto_pull: options[:auto_pull],
      auto_push: options[:auto_push],
      force_push: options[:force_push] || false
    }
    config[:commit_message] = options[:commit_message] if options[:commit_message]
    
    # Git自動化を実行
    automation = GitAutomation.new(options[:repo_path], config)
    automation.run_startup_sequence
    
  rescue Interrupt
    puts "\n処理が中断されました"
    exit(1)
  rescue => e
    puts "エラー: #{e.message}"
    exit(1)
  end
end