※当サイトの記事には、広告・プロモーションが含まれます。

OxidizedはローカルのGitリポジトリが存在しない場合git init相当をしておりリモートと同期されない罠

nazology.kusuguru.co.jp

「ありがとう」「お願いします」という感謝や丁寧な言葉は、円滑な人間関係を構築する上で欠かせません。

ChatGPTに使う「〇〇」「△△」という言葉が実はOpenAI社の負担になっていた【優しい言葉は無駄なのか】 - ナゾロジー

2025年4月17日、OpenAIのCEOサム・アルトマン氏はXにて、ChatGPTに投げかけられる「please」や「thank you」といった言葉が大きな損失を生んでいることを認めました。

ChatGPTに使う「〇〇」「△△」という言葉が実はOpenAI社の負担になっていた【優しい言葉は無駄なのか】 - ナゾロジー

⇧ 根拠となるデータが示されていないので憶測の域を出ていないようなのだが...

そもそも、実際に「電力消費」に影響していて、コストが上昇しているのであれば、公式サイトでアナウンスがあるような気がしますけど、アナウンスが無いのであれば、「OpenAI」社にとっては全くもって負担になることの無い微々たる影響しかないってことになるんですかね?

利用者側からしたら、「幻覚(ハルシネーション)」が抑制されていてくれる方が重要事項ですからな...

「幻覚(ハルシネーション)」の発生が0になれば、膨大な裏付けの作業コストが無くなって、本質的な作業に注力できますからな...

OxidizedはローカルのGitリポジトリが存在しない場合git init相当をしておりリモートと同期されない罠

Ruby」製のライブラリ「Oxidized」というものがあるのだが、公式のドキュメントの情報が不足している、且つ、整理されていないが過ぎてカオスなのだが、公式のドキュメントに載っていないが把握していないと危険な情報について備忘録として記載しておく。

ネットの情報を漁っていたところ、

github.com

⇧ とあり、バグではなく「Oxidized」の仕様というのが「Oxidized」の開発に関係する側の主張でありますと。

つまり、デリバリ手順的には、必ず「Oxidized」を起動させる前に、明示的に「git clone 」して最新の「.git/」に差し替えておく必要があるということらしい。

正確には、「Oxidized」の内部で「Rugged」というライブラリで「Git」の「bare repository」を扱っているようなので、「.git/」以外は削除しておく必要があるという部分に言及が無いため、「issue#2033」の回答は不十分なのだが...

ちなみに、

  1. git init
  2. git clone

の違いについては、

hokaccha.hatenablog.com

⇧ 上記サイト様がまとめてくださっています。

 

「Oxidized」に話を戻すと、ザックリ言うと、「Oxidized」は「リモート」の「Gitリポジトリ」の内容を事前に同期できないという残念な仕様になっているらしい。

つまり、「リモート」の「Gitリポジトリ」の「コミット履歴」が無かったことにされてしまうという、あり得ない仕様になっているのである。

「リモート」の「Gitリポジトリ」で「private repository」になっているものに対しては「認証情報」が必要になって来るのだが、「Oxidized」の「ソースコード」の「変更容易性」が低いためなのか、理由は定かでは無いのだが、

  1. git clone相当の処理(Oxidizedではbare repositoryを扱っているため、実際には、git cloneは利用できない)の追加

を「Oxidized」側で対応できていないというのが実情のようだ...

「Oxidized」としては、

⇧「git fetch」相当の処理が為されていないということみたいね...

ここで今一度「https://github.com/ytti/oxidized/issues/2033」の回答部分に着目してみると、

Need more information about your migration of Oxidized to a new server. You should be able to copy everything as-is, including the .git directory. Why are you manually initialising a new repo on your new server? Why aren't you letting Oxidized do it?

I assume it's because you want to retain git history, in which case you only need to clone your remote repo to your local oxidized.git. That's how I've done it many times myself.

https://github.com/ytti/oxidized/issues/2033

⇧『Why are you manually initialising a new repo on your new server? Why aren't you letting Oxidized do it?』が疑問形になっているのが全く理解できないのだが、大事故に繋がるということを強調して欲しい気はする...

 

『I assume it's because you want to retain git history, in which case you only need to clone your remote repo to your local oxidized.git. That's how I've done it many times myself.』については、正しい手順を公式のドキュメントに記載しておいて欲しい気はする...

 

ちなみに、「Ruby」に詳しくないので推測になってしまいますが、「Oxidized」で「Gitリポジトリ」の操作部分は、以下で行われているっぽい。

github.com

https://github.com/ytti/oxidized/blob/master/lib/oxidized/output/git.rb

module Oxidized
  module Output
    class Git < Output
      using Refinements

      class GitError < OxidizedError; end
      begin
        require 'rugged'
      rescue LoadError
        raise OxidizedError, 'rugged not found: sudo gem install rugged'
      end

      attr_reader :commitref

      def initialize
        super
        @cfg = Oxidized.config.output.git
      end

      def setup
        if @cfg.empty?
          Oxidized.asetus.user.output.git.user  = 'Oxidized'
          Oxidized.asetus.user.output.git.email = 'o@example.com'
          Oxidized.asetus.user.output.git.repo = File.join(Config::ROOT, 'oxidized.git')
          Oxidized.asetus.save :user
          raise NoConfig, "no output git config, edit #{Oxidized::Config.configfile}"
        end

        if @cfg.repo.respond_to?(:each)
          @cfg.repo.each do |group, repo|
            @cfg.repo["#{group}="] = File.expand_path repo
          end
        else
          @cfg.repo = File.expand_path @cfg.repo
        end
      end

      def store(file, outputs, opt = {})
        @msg   = opt[:msg]
        @user  = opt[:user]  || @cfg.user
        @email = opt[:email] || @cfg.email
        @opt   = opt
        @commitref = nil
        repo = @cfg.repo

        outputs.types.each do |type|
          type_cfg = ''
          type_repo = File.join(File.dirname(repo), type + '.git')
          outputs.type(type).each do |output|
            (type_cfg << output; next) unless output.name # rubocop:disable Style/Semicolon
            type_file = file + '--' + output.name
            if @cfg.type_as_directory?
              type_file = type + '/' + type_file
              type_repo = repo
            end
            update type_repo, type_file, output
          end
          update type_repo, file, type_cfg
        end

        update repo, file, outputs.to_cfg
      end

      # Returns the configuration of group/node_name
      #
      # #fetch is called by Nodes#fetch
      # Nodes#fetch creates a new Output object each time, so it not easy
      # to cache the repo index in memory. But as we keep the repo index up
      # to date on disk in #update_repo, we can read it from disk instead of
      # rebuilding it each time.
      def fetch(node, group)
        repo, path = yield_repo_and_path(node, group)
        repo = Rugged::Repository.new repo
        # Read the index from disk
        index = repo.index

        repo.read(index.get(path)[:oid]).data
      rescue StandardError
        'node not found'
      end

      # give a hash of all oid revisions for the given node, and the date of
      # the commit.
      #
      # Called by Nodes#version
      def version(node, group)
        repo_path, node_path = yield_repo_and_path(node, group)
        self.class.hash_list(node_path, repo_path)
      rescue StandardError
        'node not found'
      end

      # give the blob of a specific revision
      def get_version(node, group, oid)
        repo, path = yield_repo_and_path(node, group)
        repo = Rugged::Repository.new repo
        repo.blob_at(oid, path).content
      rescue StandardError
        'version not found'
      end

      # give a hash with the patch of a diff between 2 revision and the stats (added and deleted lines)
      def get_diff(node, group, oid1, oid2)
        diff_commits = nil
        repo, = yield_repo_and_path(node, group)
        repo = Rugged::Repository.new repo
        commit = repo.lookup(oid1)

        if oid2
          commit_old = repo.lookup(oid2)
          diff = repo.diff(commit_old, commit)
          diff.each do |patch|
            if /#{node.name}\s+/ =~ patch.to_s.lines.first
              diff_commits = { patch: patch.to_s, stat: patch.stat }
              break
            end
          end
        else
          stat = commit.parents[0].diff(commit).stat
          stat = [stat[1], stat[2]]
          patch = commit.parents[0].diff(commit).patch
          diff_commits = { patch: patch, stat: stat }
        end

        diff_commits
      rescue StandardError
        'no diffs'
      end

      # Return the list of oids for node_path in the repository repo_path
      def self.hash_list(node_path, repo_path)
        update_cache(repo_path)
        @gitcache[repo_path][:nodes][node_path] || []
      end

      # Update @gitcache, a class instance variable, ensuring persistence
      # by saving the cache independently of object instances
      def self.update_cache(repo_path)
        # initialize our cache as a class instance variable
        @gitcache ||= {}
        # When single_repo == false, we have multiple repositories
        unless @gitcache[repo_path]
          @gitcache[repo_path] = {}
          @gitcache[repo_path][:nodes] = {}
          @gitcache[repo_path][:last_commit] = nil
        end

        repo = Rugged::Repository.new repo_path

        walker = Rugged::Walker.new(repo)
        walker.sorting(Rugged::SORT_DATE)
        walker.push(repo.head.target.oid)

        # We store the commits into a temporary cache. It will be prepended
        # to @gitcache to preserve the order of the commits.
        cache = {}
        walker.each do |commit|
          if commit.oid == @gitcache[repo_path][:last_commit]
            # we have reached the last cached commit, so we're done
            break
          end

          commit.diff.each_delta do |delta|
            next unless delta.added? || delta.modified?

            hash = {}
            # We keep :date for reverse compatibility on oxidized-web <= 0.15.1
            hash[:date] = commit.time.to_s
            # date as a Time instance for more flexibility in oxidized-web
            hash[:time] = commit.time
            hash[:oid] = commit.oid
            hash[:author] = commit.author
            hash[:message] = commit.message

            filename = delta.new_file[:path]
            if cache[filename]
              cache[filename].append hash
            else
              cache[filename] = [hash]
            end
          end
        end

        cache.each_pair do |filename, hashlist|
          if @gitcache[repo_path][:nodes][filename]
            # using the splat operator (*) should be OK as hashlist should
            # not be very big when working on deltas
            @gitcache[repo_path][:nodes][filename].prepend(*hashlist)
          else
            @gitcache[repo_path][:nodes][filename] = hashlist
          end
        end

        # Store the most recent commit
        @gitcache[repo_path][:last_commit] = repo.head.target.oid
      end

      # Currently only used in unit tests
      def self.clear_cache
        @gitcache = nil
      end

      private

      def yield_repo_and_path(node, group)
        repo, path = node.repo, node.name

        path = "#{group}/#{node.name}" if group && !group.empty? && @cfg.single_repo?

        [repo, path]
      end

      def update(repo, file, data)
        return if data.empty?

        if @opt[:group]
          if @cfg.single_repo?
            file = File.join @opt[:group], file
          else
            repo = if repo.is_a?(::String)
                     File.join File.dirname(repo), @opt[:group] + '.git'
                   else
                     repo[@opt[:group]]
                   end
          end
        end

        begin
          repo = Rugged::Repository.new repo
          update_repo repo, file, data
        rescue Rugged::OSError, Rugged::RepositoryError => e
          begin
            Rugged::Repository.init_at repo, :bare
          rescue StandardError => create_error
            raise GitError, "first '#{e.message}' was raised while opening git repo, then '#{create_error.message}' was while trying to create git repo"
          end
          retry
        end
      end

      # Uploads data into file in the repository repo
      #
      # update_repo caches the index on disk. An index is usually used in a
      # working directory and not in a bare repository, which confuses users.
      # The alternative would be to rebuild the index each time, which a little
      # time consuming. Caching the index in memory is difficult because a new
      # Output object is created each time #store is called.
      def update_repo(repo, file, data)
        oid_old = repo.blob_at(repo.head.target_id, file) rescue nil
        return false if oid_old && (oid_old.content.b == data.b)

        oid = repo.write data, :blob
        # Read the index from disk
        index = repo.index
        index.add path: file, oid: oid, mode: 0o100644

        repo.config['user.name']  = @user
        repo.config['user.email'] = @email
        @commitref = Rugged::Commit.create(repo,
                                           tree:       index.write_tree(repo),
                                           message:    @msg,
                                           parents:    repo.empty? ? [] : [repo.head.target].compact,
                                           update_ref: 'HEAD')

        index.write
        true
      end
    end
  end
end

⇧ 上記のクラスにおいて、処理的には「git commit」相当までを担当しているようで、「git push」部分に関しては、

github.com

https://github.com/ytti/oxidized/blob/master/lib/oxidized/hook/githubrepo.rb

require 'rugged'

class GithubRepo < Oxidized::Hook
  def validate_cfg!
    raise KeyError, 'hook.remote_repo is required' unless cfg.has_key?('remote_repo')
  end

  def run_hook(ctx)
    unless ctx.node.repo
      log "Oxidized output is not git, can't push to remote", :error
      return
    end
    repo  = Rugged::Repository.new(ctx.node.repo)
    creds = credentials(ctx.node)
    url   = remote_repo(ctx.node)

    if url.nil? || url.empty?
      log "No repository defined for #{ctx.node.group}/#{ctx.node.name}", :error
      return
    end

    log "Pushing local repository(#{repo.path})..."
    log "to remote: #{url}"

    if repo.remotes['origin'].nil?
      repo.remotes.create('origin', url)
    elsif repo.remotes['origin'].url != url
      repo.remotes.set_url('origin', url)
    end
    remote = repo.remotes['origin']

    begin
      fetch_and_merge_remote(repo, creds)
      remote.push([repo.head.name], credentials: creds)
    rescue Rugged::NetworkError => e
      if e.message == 'unsupported URL protocol'
        log "Rugged does not support the git URL '#{url}'.", :warn
        unless Rugged.features.include?(:ssh)
          log "Note: Rugged isn't installed with ssh support. You may need " \
              '"gem install rugged -- --with-ssh"', :warn
        end
      end
      # re-raise exception for the calling method
      raise
    end
  end

  def fetch_and_merge_remote(repo, creds)
    result = repo.fetch('origin', [repo.head.name], credentials: creds)
    log result.inspect, :debug

    their_branch = remote_branch(repo)

    unless their_branch
      log 'remote branch does not exist yet, nothing to merge', :debug
      return
    end

    result = repo.merge_analysis(their_branch.target_id)

    if result.include? :up_to_date
      log 'nothing to merge', :debug
      return
    end

    log "merging fetched branch #{their_branch.name}", :debug

    merge_index = repo.merge_commits(repo.head.target_id, their_branch.target_id)

    if merge_index.conflicts?
      log("Conflicts detected, skipping Rugged::Commit.create", :warn)
      return
    end

    Rugged::Commit.create(repo,
                          parents:    [repo.head.target, their_branch.target],
                          tree:       merge_index.write_tree(repo),
                          message:    "Merge remote-tracking branch '#{their_branch.name}'",
                          update_ref: "HEAD")
  end

  private

  def credentials(node)
    Proc.new do |_url, username_from_url, _allowed_types| # rubocop:disable Style/Proc
      git_user = cfg.has_key?('username') ? cfg.username : (username_from_url || 'git')
      if cfg.has_key?('password')
        log "Authenticating using username and password as '#{git_user}'", :debug
        Rugged::Credentials::UserPassword.new(username: git_user, password: cfg.password)
      elsif cfg.has_key?('privatekey')
        pubkey = cfg.has_key?('publickey') ? cfg.publickey : nil
        log "Authenticating using ssh keys as '#{git_user}'", :debug
        rugged_sshkey(git_user: git_user, privkey: cfg.privatekey, pubkey: pubkey)
      elsif cfg.has_key?('remote_repo') && cfg.remote_repo.has_key?(node.group) && cfg.remote_repo[node.group].has_key?('privatekey')
        pubkey = cfg.remote_repo[node.group].has_key?('publickey') ? cfg.remote_repo[node.group].publickey : nil
        log "Authenticating using ssh keys as '#{git_user}' for '#{node.group}/#{node.name}'", :debug
        rugged_sshkey(git_user: git_user, privkey: cfg.remote_repo[node.group].privatekey, pubkey: pubkey)
      else
        log "Authenticating using ssh agent as '#{git_user}'", :debug
        Rugged::Credentials::SshKeyFromAgent.new(username: git_user)
      end
    end
  end

  def rugged_sshkey(args = {})
    git_user   = args[:git_user]
    privkey    = args[:privkey]
    pubkey     = args[:pubkey] || (privkey + '.pub')
    Rugged::Credentials::SshKey.new(username:   git_user,
                                    publickey:  File.expand_path(pubkey),
                                    privatekey: File.expand_path(privkey),
                                    passphrase: ENV.fetch("OXIDIZED_SSH_PASSPHRASE", nil))
  end

  def remote_repo(node)
    if node.group.nil? || cfg.remote_repo.is_a?(String)
      cfg.remote_repo
    elsif cfg.remote_repo[node.group].is_a?(String)
      cfg.remote_repo[node.group]
    elsif cfg.remote_repo[node.group].url.is_a?(String)
      cfg.remote_repo[node.group].url
    end
  end

  # Returns a Rugged::Branch to the remote branch or nil if it doen't exist
  def remote_branch(repo)
    head_branch = repo.branches[repo.head.name]
    repo.branches['origin/' + head_branch.name]
  end
end    

⇧ で行っているようなのだが、「HTTP」リクエストの際の「プロキシ」には対応していないらしい...

「HTTP」リクエストの際の「プロキシ」に対応したい場合は、独自に「git push」部分を実装する必要があるという残念な仕様になっている...

で、「Rugged」について、

stackoverflow.com

⇧ とあることから、「Oxidized」では「リモート」と同期できていないことが分かる。

「Oxidized」に生殺与奪を託してしまうと、コミット履歴が無かったことにされる、つまり「リモート」の「Gitリポジトリ」が破壊されるという恐ろしい罠があるという...

「Oxidized」の起動前に必ず、「git clone」相当の処理を行うようにして、「リモート」の「Gitリポジトリ」の内容を「ローカル」の「Gitリポジトリ」に反映しておくのを忘れないように注意ですね...

 

とりあえず、公式のドキュメントの情報が不足し過ぎていて、「ファインダビリティ(Findability)」以前の問題なのだが、「アーキテクチャ図」を公開して欲しいですな...

毎度モヤモヤ感が半端ない…

今回はこのへんで。