Module: VivlioStarter::CLI::ScaffoldBase

Extended by:
ScaffoldBase
Included in:
ScaffoldBase
Defined in:
lib/vivlio_starter/cli/scaffold_base.rb

Constant Summary collapse

BASE_RELATIVE =

保存基準の置き場所(.cache/vs/scaffold-base/)。 キャッシュなので vs clean --cache で消えるが、経路 A と 2-way が控えている。

File.join(Common::CACHE_DIR, 'scaffold-base')
MERGEABLE_EXTENSIONS =

3-way マージの材料になり得る拡張子。著者が手で編集する設定・スタイルだけを 対象にする。SVG は文字列としてはテキストだが、雛形の 3,714 個はすべて twemoji の 生成物で、著者が編集して競合することはない(保存量が 1MB → 数十 MB に跳ねる)。

%w[.css .js .json .md .txt .yaml .yml].freeze
MERGEABLE_BASENAMES =

拡張子を持たない設定ファイル

%w[Gemfile .gitignore].freeze
MAX_MERGEABLE_BYTES =

マージ対象の上限。設定ファイルとしては十分に大きく、事故的な巨大ファイルは弾く

1_048_576

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#gem_pathsObject



80
# File 'lib/vivlio_starter/cli/scaffold_base.rb', line 80

def gem_paths = @gem_paths || Gem.path

Instance Method Details

#ancestor_path(relative, expected_digest, roots) ⇒ String?

lock が記録したハッシュと中身が一致する祖先ファイルのパスを返す。

Parameters:

  • expected_digest (String, nil)

    lock に記録された "sha256:…"

Returns:

  • (String, nil)

    見つからなければ nil(呼び出し側は 2-way へ落とす)



98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/vivlio_starter/cli/scaffold_base.rb', line 98

def ancestor_path(relative, expected_digest, roots)
  return nil if expected_digest.nil? || !mergeable?(relative)

  roots.each do |root|
    path = File.join(root, relative)
    next unless File.file?(path)
    next unless ScaffoldLock.file_digest(path) == expected_digest

    return path
  end
  nil
end

#ancestor_roots(project_root = '.') ⇒ Array<String>

祖先の探索先を優先順に返す。プロジェクト内の保存基準を先に見るのは 単に近いからで、正しさは中身のハッシュ照合が担保する(順序に依存しない)。

Returns:

  • (Array<String>)

    存在するディレクトリのみ



71
72
73
# File 'lib/vivlio_starter/cli/scaffold_base.rb', line 71

def ancestor_roots(project_root = '.')
  [base_dir(project_root), *installed_scaffold_dirs].select { Dir.exist?(it) }
end

#base_dir(project_root = '.') ⇒ Object



75
# File 'lib/vivlio_starter/cli/scaffold_base.rb', line 75

def base_dir(project_root = '.') = File.join(project_root, BASE_RELATIVE)

#installed_scaffold_dirsObject

インストール済み vivlio-starter の雛形ディレクトリ(新しい版から順に)。 Bundler 実行下でもバンドル外の版を見たいので Gem.path を直接 glob する (Gem::Specification.find_by_name はバンドル内に限定されるため使えない)。



85
86
87
88
89
90
91
92
93
# File 'lib/vivlio_starter/cli/scaffold_base.rb', line 85

def installed_scaffold_dirs
  gem_paths
     .flat_map { Dir.glob(File.join(it, 'gems', 'vivlio-starter-*', 'lib', 'project_scaffold')) }
     .select { Dir.exist?(it) }
     .sort
     .reverse
rescue StandardError
  []
end

#merge(base_path, mine_path, theirs_path) ⇒ String?

git merge-file による 3-way マージ。衝突なく合流できたときだけ中身を返す。 衝突・git 不在・その他の異常はすべて nil で、呼び出し側は 2-way の競合へ落とす (衝突マーカーが著者のファイルへ紛れ込むことは無い)。

自前の diff3 は書かない——マージは境界条件が多く、枯れた実装に任せるほうが安い。 git は Xcode Command Line Tools に同梱で、vs doctor が CLT の有無を既に見ている。

Returns:

  • (String, nil)

    合流後の中身(バイト列)



118
119
120
121
122
123
124
125
# File 'lib/vivlio_starter/cli/scaffold_base.rb', line 118

def merge(base_path, mine_path, theirs_path)
  merged, _err, status = Open3.capture3(
    'git', 'merge-file', '-p', '-q', mine_path, base_path, theirs_path
  )
  status.success? ? merged : nil
rescue StandardError
  nil
end

#mergeable?(relative) ⇒ Boolean

3-way マージの材料になり得る雛形ファイルか。 著者データ領域(contents/ 等)はそもそも競合に分類されないため対象外。

Returns:

  • (Boolean)


60
61
62
63
64
65
66
# File 'lib/vivlio_starter/cli/scaffold_base.rb', line 60

def mergeable?(relative)
  return false if ScaffoldLock.author_data?(relative)

  basename = File.basename(relative)
  MERGEABLE_BASENAMES.include?(basename) ||
    MERGEABLE_EXTENSIONS.include?(File.extname(basename).downcase)
end

#sync!(project_root, scaffold_source:, lock_files:) ⇒ Object

次回の祖先として雛形を保管する。複製するのは lock が「現在の雛形と一致」と 記録したファイルだけ——記録が進まなかったファイル(競合をスキップした等)の 基準を新版で上書きすると、次回のマージが雛形側の変更を黙って捨てる。

Parameters:

  • lock_files (Hash{String => String})

    書き込み後の lock の files 節



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/vivlio_starter/cli/scaffold_base.rb', line 131

def sync!(project_root, scaffold_source:, lock_files:)
  destination_root = base_dir(project_root)

  ScaffoldLock.scaffold_files(scaffold_source).each do |relative|
    next unless mergeable?(relative)

    source = File.join(scaffold_source, relative)
    next if File.size(source) > MAX_MERGEABLE_BYTES
    next unless lock_files[relative] == ScaffoldLock.file_digest(source)

    destination = File.join(destination_root, relative)
    FileUtils.mkdir_p(File.dirname(destination))
    FileUtils.cp(source, destination)
  end
rescue StandardError => e
  # 基準の保管は「次回が楽になる」だけの処理。失敗しても upgrade 自体は成功させる
  Common.log_debug("[upgrade] 雛形の基準を保管できませんでした: #{e.message}")
end