captive-release 0.3.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/captive_release/build_command.rb +52 -0
  3. data/lib/captive_release/build_config.rb +20 -0
  4. data/lib/captive_release/build_config_prompt.rb +31 -0
  5. data/lib/captive_release/build_dispatch_run.rb +35 -0
  6. data/lib/captive_release/cli.rb +27 -382
  7. data/lib/captive_release/config.rb +2 -44
  8. data/lib/captive_release/config_file.rb +30 -0
  9. data/lib/captive_release/config_loader.rb +34 -0
  10. data/lib/captive_release/config_writer.rb +45 -0
  11. data/lib/captive_release/framework_detector.rb +30 -0
  12. data/lib/captive_release/git_commands.rb +18 -27
  13. data/lib/captive_release/git_release.rb +24 -0
  14. data/lib/captive_release/github_api.rb +45 -0
  15. data/lib/captive_release/github_dispatch.rb +12 -27
  16. data/lib/captive_release/github_http.rb +34 -25
  17. data/lib/captive_release/github_release.rb +16 -32
  18. data/lib/captive_release/github_repo.rb +29 -0
  19. data/lib/captive_release/github_token.rb +18 -0
  20. data/lib/captive_release/help.rb +50 -0
  21. data/lib/captive_release/init_command.rb +44 -0
  22. data/lib/captive_release/menu.rb +33 -0
  23. data/lib/captive_release/options.rb +27 -0
  24. data/lib/captive_release/release_command.rb +42 -0
  25. data/lib/captive_release/release_preconditions.rb +24 -0
  26. data/lib/captive_release/release_publication.rb +30 -0
  27. data/lib/captive_release/repo_files.rb +35 -0
  28. data/lib/captive_release/shell.rb +20 -0
  29. data/lib/captive_release/terminal.rb +37 -0
  30. data/lib/captive_release/version.rb +1 -1
  31. data/lib/captive_release/version_choice.rb +56 -0
  32. data/lib/captive_release/version_detector.rb +26 -57
  33. data/lib/captive_release/version_file.rb +14 -79
  34. data/lib/captive_release/version_file_bootstrap.rb +25 -0
  35. data/lib/captive_release/version_file_selection.rb +53 -0
  36. data/lib/captive_release/version_files_argument.rb +21 -0
  37. data/lib/captive_release/version_files_writing.rb +22 -0
  38. data/lib/captive_release/version_format.rb +86 -0
  39. data/lib/captive_release.rb +29 -0
  40. metadata +29 -1
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveRelease
4
+ # Le geste de release : bumper les fichiers de version, commiter, taguer, pousser, publier.
5
+ #
6
+ # Cette unité change quand le geste change, la CLI quand l'ensemble des commandes ou le
7
+ # parsing des options change.
8
+ class ReleaseCommand
9
+ def initialize(terminal:, config:, token_provider:)
10
+ @terminal = terminal
11
+ @config = config
12
+ @token_provider = token_provider
13
+ end
14
+
15
+ def run(bump_arg, confirmation:)
16
+ ReleasePreconditions.new(terminal: @terminal).verifier!
17
+ courante = @config.current_version
18
+ @terminal.say "\n#{@terminal.bold("captive-release")}\n"
19
+ @terminal.say "Current version: #{@terminal.bold(courante)}\n"
20
+
21
+ prochaine = VersionChoice.new(terminal: @terminal).call(courante, bump_arg)
22
+ confirmer!(prochaine) if confirmation
23
+
24
+ VersionFilesWriting.new(terminal: @terminal).call(prochaine, @config.version_files)
25
+ GitRelease.new(terminal: @terminal).call(prochaine, @config.version_files.map(&:path))
26
+ publication.call(prochaine)
27
+ @terminal.say "\n🎉 #{@terminal.bold("Version v#{prochaine} released successfully!")}\n"
28
+ end
29
+
30
+ private
31
+
32
+ def confirmer!(version)
33
+ @terminal.say "\nNew version: #{@terminal.bold(version)}"
34
+ reponse = @terminal.ask("Confirm? [Y/n] ").downcase
35
+ exit 1 unless reponse == "y" || reponse.empty?
36
+ end
37
+
38
+ def publication
39
+ ReleasePublication.new(terminal: @terminal, token_provider: @token_provider)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveRelease
4
+ # Ce qui doit être vrai avant qu'une release parte.
5
+ #
6
+ # Le commit de version part avec --only, mais un arbre sale reste un mauvais point de
7
+ # départ : le tag désignerait un état que personne n'a relu.
8
+ #
9
+ # Cette unité change quand les conditions changent, ReleaseCommand quand la suite des
10
+ # étapes change.
11
+ class ReleasePreconditions
12
+ def initialize(terminal:)
13
+ @terminal = terminal
14
+ end
15
+
16
+ def verifier!
17
+ return unless GitCommands.dirty?
18
+
19
+ @terminal.say @terminal.error("L'arbre de travail n'est pas propre.")
20
+ @terminal.say "Commitez ou remisez vos modifications avant de releaser.\n"
21
+ exit 1
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveRelease
4
+ # Publie la release sur GitHub, ou dit pourquoi elle ne l'est pas. Le jeton arrive par un
5
+ # fournisseur et non par une valeur, pour n'être réclamé qu'au moment où il sert.
6
+ #
7
+ # Cette unité change quand les règles de publication changent, ReleaseCommand quand les
8
+ # étapes du geste changent.
9
+ class ReleasePublication
10
+ def initialize(terminal:, token_provider:)
11
+ @terminal = terminal
12
+ @token_provider = token_provider
13
+ end
14
+
15
+ def call(version)
16
+ @terminal.say "\n#{@terminal.bold("Creating GitHub release...")}"
17
+ jeton = @token_provider.call
18
+ return sans_jeton unless jeton
19
+
20
+ url = GithubRelease.create(version: version, remote_url: GitCommands.remote_url, token: jeton)
21
+ @terminal.done url
22
+ end
23
+
24
+ private
25
+
26
+ def sans_jeton
27
+ @terminal.say " #{@terminal.warn("⚠️ GITHUB_TOKEN not set - skipping GitHub release creation.")}"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module CaptiveRelease
6
+ # Où chercher dans un dépôt, et où ne pas chercher.
7
+ #
8
+ # Cette unité change quand la liste des dossiers à ignorer change. Sans elle, chaque
9
+ # détection réimplémentait l'exclusion, et en oublier une faisait remonter un
10
+ # node_modules/**/app.json comme s'il était celui du projet.
11
+ class RepoFiles
12
+ EXCLUDED_DIRS = %w[node_modules .git vendor tmp coverage dist .next .nuxt Pods].freeze
13
+
14
+ def initialize(root)
15
+ @root = Pathname.new(root).expand_path
16
+ end
17
+
18
+ def path(*segments) = @root.join(*segments)
19
+
20
+ def glob(*segments)
21
+ Dir.glob(path(*segments).to_s).sort.reject { |chemin| excluded?(chemin) }
22
+ end
23
+
24
+ def relative(chemin) = Pathname.new(chemin).relative_path_from(@root).to_s
25
+
26
+ private
27
+
28
+ def excluded?(chemin)
29
+ relatif = relative(chemin)
30
+ EXCLUDED_DIRS.any? do |dossier|
31
+ relatif.start_with?("#{dossier}/") || relatif.include?("/#{dossier}/")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveRelease
4
+ # Comment on parle au shell : échapper ce qu'on lui passe, lire ce qu'il rend, et échouer
5
+ # bruyamment quand la commande échoue.
6
+ #
7
+ # Cette unité change quand la façon de lancer une commande change, GitCommands quand les
8
+ # commandes d'une release changent.
9
+ module Shell
10
+ def self.run!(commande)
11
+ system(commande) || raise("Command failed: #{commande}")
12
+ end
13
+
14
+ def self.capture(commande) = `#{commande}`.strip
15
+
16
+ # Rubocop veut Shellwords, mais son échappement diffère du nôtre sur des chemins que git
17
+ # accepte : la liste blanche dit exactement ce qui passe sans protection.
18
+ def self.escape(chaine) = chaine.gsub(%r{[^a-zA-Z0-9._\-/]}) { |caractere| "\\#{caractere}" }
19
+ end
20
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveRelease
4
+ # Tout ce qui touche au terminal : la couleur et la saisie.
5
+ #
6
+ # Cette unité change quand l'habillage change, les commandes quand leur geste change. Les
7
+ # deux flux sont injectés, ce qui rend chaque commande testable sans toucher à $stdin.
8
+ class Terminal
9
+ def initialize(input: $stdin, output: $stdout)
10
+ @input = input
11
+ @output = output
12
+ end
13
+
14
+ def say(message = "") = @output.puts(message)
15
+
16
+ def prompt(message) = @output.print(message)
17
+
18
+ def ask(message = "> ")
19
+ prompt(message)
20
+ @input.gets&.chomp || ""
21
+ end
22
+
23
+ # Un choix hors menu vaut 0, qui ne correspond à aucune entrée : l'appelant décide quoi
24
+ # en faire plutôt que de voir une exception.
25
+ def ask_choice(message = "\n> ") = ask(message).to_i
26
+
27
+ # La ligne d'une étape réussie : six unités la réécrivaient, et leurs deux espaces
28
+ # finissaient par ne plus être les mêmes.
29
+ def done(etape) = say(" #{ok("✅")} #{etape}")
30
+
31
+ def bold(str) = "\e[1m#{str}\e[0m"
32
+ def dim(str) = "\e[2m#{str}\e[0m"
33
+ def ok(str) = "\e[32m#{str}\e[0m"
34
+ def warn(str) = "\e[33m#{str}\e[0m"
35
+ def error(str) = "\e[31m#{str}\e[0m"
36
+ end
37
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CaptiveRelease
4
- VERSION = "0.3.2"
4
+ VERSION = "0.4.4"
5
5
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveRelease
4
+ # Comment on désigne la prochaine version : un mot-clé de bump, un semver écrit à la main,
5
+ # ou un menu quand rien n'est donné.
6
+ #
7
+ # Cette unité change quand les façons de nommer une version changent, ReleaseCommand quand
8
+ # le geste de release change.
9
+ class VersionChoice
10
+ BUMP_TYPES = %w[patch minor major].freeze
11
+ SEMVER_RE = /\A\d+\.\d+\.\d+\z/
12
+ CUSTOM = "Custom"
13
+
14
+ def initialize(terminal:)
15
+ @terminal = terminal
16
+ end
17
+
18
+ def call(current, bump_arg)
19
+ bump_arg ? resolve(current, bump_arg) : ask(current)
20
+ end
21
+
22
+ private
23
+
24
+ def resolve(current, type)
25
+ return VersionBumper.bump(current, type) if type.match?(SEMVER_RE) || BUMP_TYPES.include?(type)
26
+
27
+ refuse("Invalid bump type: #{type}. Use patch|minor|major|x.y.z")
28
+ end
29
+
30
+ def ask(current)
31
+ choisi = Menu.new(terminal: @terminal).choose("Choose release type:", candidates_for(current))
32
+ choisi == CUSTOM ? type_it : choisi
33
+ end
34
+
35
+ # La valeur rendue est la version elle-même, sauf pour Custom qui n'en a pas encore une.
36
+ def candidates_for(current)
37
+ candidats = BUMP_TYPES.to_h do |type|
38
+ version = VersionBumper.bump(current, type)
39
+ [ "#{type.capitalize} → #{@terminal.bold(version)}", version ]
40
+ end
41
+ candidats.merge("#{CUSTOM} → ?" => CUSTOM)
42
+ end
43
+
44
+ def type_it
45
+ version = @terminal.ask("Enter version (x.y.z): ")
46
+ return version if version.match?(SEMVER_RE)
47
+
48
+ refuse("Invalid semver: #{version}")
49
+ end
50
+
51
+ def refuse(message)
52
+ @terminal.say @terminal.error(message)
53
+ exit 1
54
+ end
55
+ end
56
+ end
@@ -1,84 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "pathname"
4
-
5
3
  module CaptiveRelease
4
+ # Quels fichiers d'un dépôt portent une version, et sous quel format.
6
5
  class VersionDetector
7
- EXCLUDED_DIRS = %w[node_modules .git vendor tmp coverage dist .next .nuxt Pods].freeze
6
+ RUBY_VERSION_PATTERN = /VERSION\s*=\s*["'].+?["']/
7
+ GRADLE_PATH = %w[android app build.gradle].freeze
8
8
 
9
9
  def self.scan(root = ".")
10
10
  new(root).scan
11
11
  end
12
12
 
13
13
  def initialize(root)
14
- @root = Pathname.new(root).expand_path
14
+ @files = RepoFiles.new(root)
15
15
  end
16
16
 
17
17
  def scan
18
- find_files("package.json", "package_json") +
19
- find_files("app.config.js", "app_config_js") +
20
- find_ruby_version_files +
21
- find_files("VERSION", "plain") +
22
- find_android_gradle_files +
23
- find_ios_plist_files
24
- end
25
-
26
- def expo?
27
- find_files("app.config.js", "app_config_js").any? ||
28
- find_files("app.json", "plain").any? { |f| File.read(File.join(@root, f.path)).include?('"expo"') }
29
- rescue
30
- false
31
- end
32
-
33
- def capacitor?
34
- %w[capacitor.config.ts capacitor.config.json capacitor.config.js].any? do |f|
35
- File.exist?(@root.join(f))
36
- end
37
- rescue
38
- false
18
+ by_name("package.json", "package_json") +
19
+ by_name("app.config.js", "app_config_js") +
20
+ by_content(%w[lib ** version.rb], "ruby_version", RUBY_VERSION_PATTERN) +
21
+ by_name("VERSION", "plain") +
22
+ android_gradle +
23
+ by_content(%w[ios ** Info.plist], "ios_plist", /CFBundleShortVersionString/)
39
24
  end
40
25
 
41
26
  private
42
27
 
43
- def find_android_gradle_files
44
- path = @root.join("android", "app", "build.gradle")
45
- return [] unless File.exist?(path) && File.read(path).match?(/versionName\s+['"]/)
46
-
47
- [ VersionFile.new("android/app/build.gradle", "android_gradle") ]
48
- rescue
49
- []
28
+ def by_name(filename, type)
29
+ @files.glob("**", filename).map { |chemin| version_file(chemin, type) }
50
30
  end
51
31
 
52
- def find_ios_plist_files
53
- Dir.glob(@root.join("ios", "**", "Info.plist").to_s).sort.reject { |p| excluded?(p) }.select do |path|
54
- File.read(path).include?("CFBundleShortVersionString")
55
- end.map do |path|
56
- relative = Pathname.new(path).relative_path_from(@root).to_s
57
- VersionFile.new(relative, "ios_plist")
58
- end
59
- rescue
32
+ def by_content(segments, type, pattern)
33
+ @files.glob(*segments)
34
+ .select { |chemin| File.read(chemin).match?(pattern) }
35
+ .map { |chemin| version_file(chemin, type) }
36
+ rescue StandardError
60
37
  []
61
38
  end
62
39
 
63
- def find_ruby_version_files
64
- Dir.glob(@root.join("lib", "**", "version.rb").to_s).sort.reject { |p| excluded?(p) }.select do |path|
65
- File.read(path).match?(/VERSION\s*=\s*["'].+?["']/)
66
- end.map do |path|
67
- relative = Pathname.new(path).relative_path_from(@root).to_s
68
- VersionFile.new(relative, "ruby_version")
69
- end
70
- end
40
+ # Le build.gradle est à un chemin fixe : le chercher par glob remonterait ceux des
41
+ # modules Android embarqués, qui ne portent pas la version de l'app.
42
+ def android_gradle
43
+ chemin = @files.path(*GRADLE_PATH)
44
+ return [] unless File.exist?(chemin) && File.read(chemin).match?(/versionName\s+['"]/)
71
45
 
72
- def find_files(filename, type)
73
- Dir.glob(@root.join("**", filename).to_s).sort.reject { |p| excluded?(p) }.map do |path|
74
- relative = Pathname.new(path).relative_path_from(@root).to_s
75
- VersionFile.new(relative, type)
76
- end
46
+ [ VersionFile.new(GRADLE_PATH.join("/"), "android_gradle") ]
47
+ rescue StandardError
48
+ []
77
49
  end
78
50
 
79
- def excluded?(path)
80
- relative = Pathname.new(path).relative_path_from(@root).to_s
81
- EXCLUDED_DIRS.any? { |dir| relative.start_with?("#{dir}/") || relative.include?("/#{dir}/") }
82
- end
51
+ def version_file(chemin, type) = VersionFile.new(@files.relative(chemin), type)
83
52
  end
84
53
  end
@@ -1,44 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
-
5
3
  module CaptiveRelease
4
+ # Un fichier qui porte une version : son chemin, son type, et les garde-fous qui disent
5
+ # au développeur ce qui manque. La façon dont la version s'exprime dans le texte vit dans
6
+ # VersionFormat, qui ne connaît pas le disque.
6
7
  class VersionFile
7
- TYPES = %w[package_json plain app_config_js ruby_version android_gradle ios_plist].freeze
8
+ TYPES = VersionFormat.types
8
9
 
9
10
  attr_reader :path, :type
10
11
 
11
12
  def initialize(path, type)
12
13
  raise ArgumentError, "Unknown type #{type}. Must be one of: #{TYPES.join(", ")}" unless TYPES.include?(type)
13
14
 
14
- @path = path
15
- @type = type
15
+ @path = path
16
+ @type = type
17
+ @format = VersionFormat.for(type)
16
18
  end
17
19
 
18
20
  def read_version
19
- raise "File not found: #{path}" unless File.exist?(path)
20
-
21
- case type
22
- when "package_json" then JSON.parse(File.read(path))["version"]
23
- when "plain" then File.read(path).strip
24
- when "app_config_js" then read_app_config_js
25
- when "ruby_version" then read_ruby_version
26
- when "android_gradle" then read_android_gradle
27
- when "ios_plist" then read_ios_plist
28
- end
21
+ @format.read(lire!) ||
22
+ raise("Cannot read version from #{path} : expected pattern: #{@format.expected}")
29
23
  end
30
24
 
31
25
  def write_version(new_version)
32
- raise "File not found: #{path}" unless File.exist?(path)
33
-
34
- case type
35
- when "package_json" then write_package_json(new_version)
36
- when "plain" then File.write(path, "#{new_version}\n")
37
- when "app_config_js" then write_app_config_js(new_version)
38
- when "ruby_version" then write_ruby_version(new_version)
39
- when "android_gradle" then write_android_gradle(new_version)
40
- when "ios_plist" then write_ios_plist(new_version)
41
- end
26
+ contenu = lire!
27
+ File.write(path, @format.write(contenu, read_version, new_version))
42
28
  end
43
29
 
44
30
  def to_h
@@ -47,61 +33,10 @@ module CaptiveRelease
47
33
 
48
34
  private
49
35
 
50
- def write_package_json(new_version)
51
- content = File.read(path)
52
- old_version = JSON.parse(content)["version"]
53
- File.write(path, content.gsub(/"version": "#{Regexp.escape(old_version)}"/, %("version": "#{new_version}")))
54
- end
55
-
56
- def read_ruby_version
57
- File.read(path).match(/VERSION\s*=\s*["'](.+?)["']/)[1]
58
- rescue
59
- raise "Cannot read version from #{path} — expected pattern: VERSION = \"x.y.z\""
60
- end
61
-
62
- def write_ruby_version(new_version)
63
- content = File.read(path)
64
- old_version = read_ruby_version
65
- File.write(path, content.gsub(/VERSION\s*=\s*["']#{Regexp.escape(old_version)}["']/) do |match|
66
- match.include?('"') ? %(VERSION = "#{new_version}") : %(VERSION = '#{new_version}')
67
- end)
68
- end
69
-
70
- def read_app_config_js
71
- File.read(path).match(/version:\s*['"](.+?)['"]/)[1]
72
- rescue
73
- raise "Cannot read version from #{path} — expected pattern: version: 'x.y.z'"
74
- end
75
-
76
- def write_app_config_js(new_version)
77
- content = File.read(path)
78
- old_version = read_app_config_js
79
- File.write(path, content.gsub(/version:\s*['"]#{Regexp.escape(old_version)}['"]/, "version: '#{new_version}'"))
80
- end
81
-
82
- def read_android_gradle
83
- File.read(path).match(/versionName\s+['"](.+?)['"]/)&.[](1) ||
84
- raise("Cannot read version from #{path} — expected pattern: versionName 'x.y.z'")
85
- end
86
-
87
- def write_android_gradle(new_version)
88
- content = File.read(path)
89
- old_version = read_android_gradle
90
- File.write(path, content.gsub(/versionName\s+['"]#{Regexp.escape(old_version)}['"]/, "versionName '#{new_version}'"))
91
- end
92
-
93
- def read_ios_plist
94
- File.read(path).match(/<key>CFBundleShortVersionString<\/key>\s*<string>(.+?)<\/string>/)&.[](1) ||
95
- raise("Cannot read version from #{path} — expected CFBundleShortVersionString key")
96
- end
36
+ def lire!
37
+ raise "File not found: #{path}" unless File.exist?(path)
97
38
 
98
- def write_ios_plist(new_version)
99
- content = File.read(path)
100
- old_version = read_ios_plist
101
- File.write(path, content.gsub(
102
- /(<key>CFBundleShortVersionString<\/key>\s*<string>)#{Regexp.escape(old_version)}(<\/string>)/,
103
- "\\1#{new_version}\\2"
104
- ))
39
+ File.read(path)
105
40
  end
106
41
  end
107
42
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveRelease
4
+ # Le dépôt ne porte aucun fichier de version : proposer d'en créer un, et le créer.
5
+ #
6
+ # Cette unité change quand ce que reçoit un dépôt nu change, InitCommand quand la façon de
7
+ # réunir les fichiers change.
8
+ class VersionFileBootstrap
9
+ VERSION_INITIALE = "0.1.0"
10
+
11
+ def initialize(terminal:)
12
+ @terminal = terminal
13
+ end
14
+
15
+ def call(motif)
16
+ @terminal.say @terminal.warn(motif)
17
+ @terminal.say "\nCreate a #{@terminal.bold("VERSION")} file? [y/N] "
18
+ exit 1 unless @terminal.ask.downcase == "y"
19
+
20
+ File.write("VERSION", "#{VERSION_INITIALE}\n")
21
+ @terminal.done "VERSION créé avec #{@terminal.bold(VERSION_INITIALE)}"
22
+ [ VersionFile.new("VERSION", "plain") ]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveRelease
4
+ # Parmi les fichiers de version détectés, lesquels retenir.
5
+ #
6
+ # La saisie nomme ce qu'on ne veut **pas** : sur un dépôt qui porte dix fichiers de
7
+ # version, exclure est plus court que tout énumérer. Cette unité change quand la façon de
8
+ # choisir change, InitCommand quand ce qu'on fait du choix change.
9
+ class VersionFileSelection
10
+ def initialize(terminal:)
11
+ @terminal = terminal
12
+ end
13
+
14
+ def call(found)
15
+ list(found)
16
+ found.size == 1 ? seul(found) : exclude(found, demander)
17
+ end
18
+
19
+ private
20
+
21
+ def list(found)
22
+ found.each_with_index do |fichier, rang|
23
+ @terminal.say " #{@terminal.dim("[#{rang + 1}]")} #{fichier.path} " \
24
+ "#{@terminal.dim("(#{readable_version(fichier)})")}"
25
+ end
26
+ end
27
+
28
+ def demander
29
+ @terminal.say "\nAppuyez sur #{@terminal.bold("Entrée")} pour tout sélectionner, " \
30
+ "ou entrez les numéros à #{@terminal.bold("exclure")} (ex: 2,3) :"
31
+ @terminal.ask
32
+ end
33
+
34
+ def seul(found)
35
+ @terminal.say ""
36
+ @terminal.done "1 fichier détecté : sélectionné automatiquement."
37
+ found
38
+ end
39
+
40
+ def exclude(found, saisie)
41
+ return found if saisie.strip.empty?
42
+
43
+ rangs = saisie.split(",").map { |numero| numero.strip.to_i - 1 }
44
+ found.reject.with_index { |_, rang| rangs.include?(rang) }
45
+ end
46
+
47
+ def readable_version(fichier)
48
+ fichier.read_version
49
+ rescue StandardError
50
+ "?"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveRelease
4
+ # Lit la valeur de `--files` : une liste de chemins séparés par des virgules, chacun pouvant
5
+ # nommer son type après un deux-points.
6
+ #
7
+ # Cette unité change quand la forme de l'argument change, InitCommand quand ce qu'on fait
8
+ # des fichiers change.
9
+ module VersionFilesArgument
10
+ def self.parse(valeur)
11
+ valeur.split(",").map(&:strip).map do |entree|
12
+ chemin, type = entree.split(":")
13
+ VersionFile.new(chemin, type || type_deduit(chemin))
14
+ end
15
+ end
16
+
17
+ def self.type_deduit(chemin)
18
+ File.basename(chemin) == "package.json" ? "package_json" : "plain"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveRelease
4
+ # Poser la nouvelle version dans chaque fichier qui la porte, et le dire au fur et à mesure :
5
+ # une release qui s'arrête doit laisser voir jusqu'où elle est allée.
6
+ #
7
+ # Cette unité change quand la façon d'écrire change, ReleaseCommand quand la suite des
8
+ # étapes change.
9
+ class VersionFilesWriting
10
+ def initialize(terminal:)
11
+ @terminal = terminal
12
+ end
13
+
14
+ def call(version, fichiers)
15
+ @terminal.say "\n#{@terminal.bold("Updating version files...")}"
16
+ fichiers.each do |fichier|
17
+ fichier.write_version(version)
18
+ @terminal.done fichier.path
19
+ end
20
+ end
21
+ end
22
+ end