From a91abd4b655cad10dfe52284aae613f11dae8fd9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 20:05:09 +0000 Subject: [PATCH 1/5] Add in-depth repository analysis document Documents the architecture, loading pipeline, and plugin bridge; assesses current health (test suite, CI, lockfile compatibility); catalogs nine empirically confirmed bugs with file/line references; and lists design flaws, security considerations, and prioritized improvements. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0159sPGBuYwWZLMknPaw6AK4 --- docs/REPO_ANALYSIS.md | 409 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 docs/REPO_ANALYSIS.md diff --git a/docs/REPO_ANALYSIS.md b/docs/REPO_ANALYSIS.md new file mode 100644 index 0000000..49f6778 --- /dev/null +++ b/docs/REPO_ANALYSIS.md @@ -0,0 +1,409 @@ +# git-commander — Repository Analysis + +*An in-depth review of how this codebase works, the value it provides, how it is designed, +and where it has flaws or room to improve.* + +- [What this project is](#what-this-project-is) +- [The value it provides](#the-value-it-provides) +- [How it works](#how-it-works) +- [Design assessment](#design-assessment) +- [Current health](#current-health) +- [Confirmed bugs](#confirmed-bugs) +- [Design flaws and rough edges](#design-flaws-and-rough-edges) +- [Security considerations](#security-considerations) +- [Recommended improvements](#recommended-improvements) + +--- + +## What this project is + +**git-commander** is a Ruby gem (v0.1.0, unreleased) that lets developers define their own +git subcommands in a small Ruby DSL and run them as `git cmd `. Command definitions +live in the repository they belong to — a `Workflow` file at the git root, or files under +`.git-commands/` — so custom workflows travel with the project and are shared with the whole +team through version control. + +A minimal command: + +```ruby +# .git-commands/current.rb +plugin :system + +command :current do |cmd| + cmd.summary "Outputs all commits not on the base branch" + cmd.argument :base_branch, default: "master" + + cmd.on_run do |options| + current_branch = system.run "git rev-parse --abbrev-ref HEAD" + system.run "git log #{options[:base_branch]}..#{current_branch}" + end +end +``` + +Running `git cmd current main` executes the `on_run` block with `options[:base_branch] == "main"`. + +## The value it provides + +The niche it targets sits between two existing options: + +- **Git aliases** are limited to one-liners, have no argument parsing, no help text, and no + way to pull in libraries. +- **Standalone scripts** (Thor CLIs, `bin/` scripts) get you all of that, but each one + reinvents option parsing, help output, and dependency setup, and they don't compose. + +git-commander's pitch is a *framework* for repo-local git tooling: + +1. **Declarative option handling** — `argument`, `flag`, and `switch` declarations with + defaults and descriptions, wired automatically into Ruby's `OptionParser` and into + generated help text. +2. **A plugin system** — plugins expose named objects (`git`, `github`, `prompt`, `system`) + inside command blocks, so command authors write against high-level APIs instead of + shelling out. Plugins can declare their own namespaced commands (`git cmd github:setup`) + and depend on other plugins. +3. **Inline gem dependencies** — command and plugin files can declare gems via Bundler's + inline `gemfile` block, so a workflow file is self-describing about what it needs. +4. **Team distribution for free** — because definitions live in the repo, `git clone` is + the installation step for everyone else. +5. **Testing support** — `GitCommander::RSpec::PluginHelpers` ships with the gem so plugin + authors can stub inline gemfiles and assert on declared gems. + +## How it works + +### Runtime flow + +``` +git cmd current main + │ + ▼ +exe/git-cmd ──── discovers & loads: ./Workflow, .git-commands/*.rb (commands) + │ .git-commands/plugins/*.rb (plugins) + ▼ +CLI#run ── Registry#find(:current) ── OptionParser parses flags/switches, + │ positional args assigned in order + ▼ +Command#run ── Runner.new(self).run(options_hash) + │ + ▼ +Runner#instance_exec(**options, &command.block) + │ + └─ method_missing(:system) → Registry#find_plugin(:system).executor + (SimpleDelegator around the plugin instance) +``` + +### Component map + +| Component | File | Responsibility | +|---|---|---| +| `exe/git-cmd` | `exe/git-cmd` | Entry point; globs command/plugin files from the working directory and loads them into a registry, then hands `ARGV` to the CLI | +| `CLI` | `lib/git_commander/cli.rb` | Maps `ARGV` to a registered command; builds an `OptionParser` from the command's declared flags/switches; prints global help on unknown commands | +| `Registry` | `lib/git_commander/registry.rb` | Hash-backed store of commands and plugins; `load(LoaderClass, *args)` runs a loader and registers its results on success | +| `Command` | `lib/git_commander/command.rb` | Holds a command's name, options (`arguments`/`flags`/`switches`), help text, and the `on_run` block | +| `Command::Option` | `lib/git_commander/command/option.rb` | Value object normalizing arguments, flags, and switches (name, default, description, value) | +| `Command::Runner` | `lib/git_commander/command/runner.rb` | Execution context for `on_run` blocks; `method_missing` resolves bare words to plugin executors | +| `Command::Configurator` | `lib/git_commander/command/configurator.rb` | Builds a `Command` from a DSL block via `instance_exec`, wrapping failures in `ConfigurationError` | +| `CommandLoaderOptions` | `lib/git_commander/command_loader_options.rb` | The DSL mixin: `summary`, `description`, `argument`, `flag`, `switch`, `on_run` | +| `Loader` / `LoaderResult` | `lib/git_commander/loader.rb`, `loader_result.rb` | Abstract loader interface; result object accumulating `commands`, `plugins`, `errors` | +| `Loaders::Raw` | `lib/git_commander/command/loaders/raw.rb` | `instance_eval`s a Ruby string; exposes `command` and `plugin` as top-level DSL keywords | +| `Loaders::FileLoader` | `lib/git_commander/command/loaders/file_loader.rb` | Reads a file and delegates to `Raw` | +| `Plugin` / `Plugin::Executor` | `lib/git_commander/plugin.rb`, `plugin/executor.rb` | Wraps a plugin's source instance in a `SimpleDelegator` for use inside command blocks | +| `Plugin::Loader` | `lib/git_commander/plugin/loader.rb` | Resolves a plugin by symbol (bundled plugins in `lib/git_commander/plugins/`) or file path; `instance_eval`s it; the file's **last expression** becomes the plugin instance | +| `System` | `lib/git_commander/system.rb` | `Open3.capture3` wrapper for shell commands with logging and error raising | +| `Logger` | `lib/git_commander/logger.rb` | File logger, defaulting to `/tmp/git-commander.log`, path configurable via `git config commander.log-file-path` | + +### The loading pipeline + +Loading is the most interesting part of the design. Everything is evaluated Ruby: + +1. `FileLoader` reads a file and passes the source string to `Raw`. +2. `Raw#load` calls `instance_eval` on the string. Inside that context: + - `command(:name) { |cmd| ... }` → `Configurator` creates a `Command` and `instance_exec`s + the block against it, so `cmd.summary`, `cmd.argument`, `cmd.on_run` etc. (from + `CommandLoaderOptions`) configure it. + - `plugin(:name)` → `Plugin::Loader` recursively loads the named plugin. +3. `Plugin::Loader#load` reads the plugin file (bundled dir for symbols, literal path for + strings) and `instance_eval`s it. The plugin file may itself call `gemfile` (Bundler + inline), `plugin` (dependencies), and `command` (namespaced as `plugin-name:command`). + Whatever the file's last expression returns is wrapped in `Plugin::Executor` + (a `SimpleDelegator`) and becomes the object command blocks talk to. +4. All results funnel into a `LoaderResult`; `Registry#load` registers the commands and + plugins only if the result has no errors. Exceptions — including `SyntaxError`, hence the + deliberate `rescue Exception` in `Raw` — are captured as result errors rather than crashing. + +### The plugin bridge + +At run time, `Runner` `undef`s Ruby's built-in `system` so that bare-word references inside +`on_run` blocks fall through to `method_missing`, which looks the name up in the registry's +plugins and returns its executor. That is how `system.run "git status"` and +`github.pull_requests(...)` work without any explicit receiver — a clean trick that keeps +the command-author API minimal. + +The four bundled plugins illustrate the range: `system` returns the `GitCommander::System` +class itself; `prompt` returns a `TTY::Prompt` instance; `git` returns a Rugged repository +wrapped so that "global" config writes go to `~/.gitconfig.commander`; `github` returns an +`Octokit::Client` and declares a `github:setup` command. + +## Design assessment + +**What's good:** + +- **Clear separation of concerns.** CLI parsing, the registry, command configuration, + execution context, and loading are all separate small classes with narrow interfaces. + The `Loader`/`LoaderResult` pattern gives uniform error accumulation across loaders. +- **The DSL is genuinely ergonomic.** A useful command is ~10 lines; option parsing and + help text come free. The `instance_exec`-with-`cmd`-yield pattern in `Configurator` + supports both `cmd.summary` and bare `summary` styles. +- **Error containment is deliberate.** A broken workflow file logs a warning instead of + breaking every other command — the right call for a tool that evaluates user-authored code. +- **Failure-tolerant loading is tested.** The spec suite (77 examples) covers loaders, + registry, CLI parsing, and end-to-end feature specs that shell out to the real executable. +- **Shipping test helpers for plugin authors** shows unusual care for the extension + ecosystem the project wants to grow. + +**Structural weaknesses (details in the flaw sections below):** + +- Everything rides on `instance_eval`/`method_missing` metaprogramming, which is flexible + but makes failures indirect and enables several of the confirmed bugs. +- There is duplicated logic that has already drifted: help-text formatting exists in both + `CLI` and `Command`; the `command`/`plugin` DSL methods are duplicated between `Raw` and + `Plugin::Loader`; `handle_error` is copy-pasted across three loaders. +- Commands are mutable singletons in the registry — parsed option *values* are written back + onto the registered `Command` object, so state leaks between runs in any long-lived process. +- Some names mislead: `System.run`'s `:blocking` option actually means "raise on failure", + and `:silent` controls echoing output to STDOUT, bypassing the command's configured + output IO entirely. + +## Current health + +Assessed 2026-07 on this checkout (`main` @ `50c0f9f`): + +- **Version 0.1.0, apparently never released** to RubyGems; the homepage still points to + `codenamev/git-commander` while the repo lives under `git-command/`. +- **Test suite: 77 examples, 2 failures** on Ruby 3.3.6. Both failures are + `Encoding::CompatibilityError`s in feature specs comparing the en-dash (`–`) in help + output against US-ASCII strings — environment/encoding artifacts, not logic failures. + On the CI matrix rubies (2.7/3.0/3.1) the suite passes. +- **The committed `Gemfile.lock` does not install on modern Ruby**: it pins `stringio 3.0.1`, + whose native extension fails to build on Ruby ≥ 3.2. `bundle install` fails out of the box + on Ruby 3.3. +- **CI**: GitHub Actions matrix covers Ruby 2.7.5/3.0.3/3.1.0 on Ubuntu and macOS (note the + stray trailing space in `'3.1.0 '`), plus a CodeQL workflow. A stale `.travis.yml` remains. +- **The gemspec declares no `required_ruby_version`** and its only runtime dependency is + `bundler`. The bundled plugins' real dependencies (`rugged`, `octokit`, `tty-prompt`) are + resolved lazily via inline gemfiles at plugin load time. + +## Confirmed bugs + +Each of these was reproduced against this checkout, not just read from the code. + +### 1. `Registry#register` silently discards its block — registered commands are no-ops + +`registry.rb:26` passes the block as an *option* (`options.merge(block: block)`), but +`Command#initialize` (`command.rb:42`) only honors a block passed as an actual Ruby block +(`block_given?`) and never reads `options[:block]`: + +```ruby +registry.register(:greet) { |opts| puts "BLOCK RAN" } +registry.find(:greet).run # => nothing; the command got the default empty proc +``` + +Every command registered through the documented `Registry#register` API does nothing when +run. The DSL path (`Configurator` → `on_run`) is unaffected, which is why the feature specs +don't catch it. Fix: `Command.new(name, registry: self, **options, &block)` or honor +`options[:block]`. + +### 2. `Runner` memoizes the first plugin lookup — multi-plugin commands get the wrong object + +`runner.rb:41` caches `@plugin_executor ||= ...` without keying by name: + +```ruby +def plugin_executor(plugin_name) + @plugin_executor ||= command.registry.find_plugin(plugin_name)&.executor +end +``` + +The first plugin referenced in an `on_run` block is returned for *every* subsequent plugin +reference. Reproduced: after resolving plugin `alpha`, a lookup for `beta` returns `alpha`'s +executor. Any command using two or more plugins — including the bundled `github` plugin, +which uses `prompt`, `github`, and `say` together — calls methods on the wrong object. +Fix: memoize in a hash keyed by `plugin_name`, e.g. `(@plugin_executors ||= {})[plugin_name] ||= ...`. + +### 3. `Option#value` can't hold `false` — switches with truthy defaults can't be turned off + +`option.rb:26–28`: + +```ruby +def value + @value || @default +end +``` + +An explicitly assigned `false` (e.g. from `--no-loud`) falls through to the default. +Reproduced: a switch with `default: true` set to `false` still reports `true`. Any boolean +switch defaulting to `true` is impossible to disable, and flags explicitly set to `nil`/`false` +silently revert. Fix: track assignment explicitly (`defined?(@value)` guard or an +`@value_set` flag). + +### 4. `Option` defines `eql?` without `hash` — the options `Set` never deduplicates + +`option.rb:30–36` implements `==` and aliases `eql?`, but never overrides `#hash`, so +`Set` falls back to identity. Reproduced: two equal options produce a `Set` of size 2. +`Command#options` (`command.rb:83`) therefore provides none of the uniqueness the docs +promise, and duplicate option names silently coexist (last one wins at merge time in +`Command#run`). Fix: `def hash; [self.class, name, default, description].hash; end`. + +### 5. `System::Command#name` is always the empty string + +`system.rb:19` splits on *word characters* instead of whitespace: + +```ruby +@name = command_with_arguments.split(/\w/i).first # "git log" → "" +``` + +Reproduced: the name of every system command is `""`, so `System.run` failures raise +`"" "" failed to run.` with the command name missing. Fix: `split(/\s/)` (or +`.split.first`). + +### 6. Configured log path keeps its trailing newline + +`logger.rb:32` reads `` `git config --get commander.log-file-path` `` and never `chomp`s. +When the setting exists, the logger opens a file whose name ends in `\n`. Fix: `.chomp`. + +### 7. `Plugin#find_command` indexes an Array as a Hash + +`Plugin::Loader` assigns `plugin.commands = @commands` (an **Array**, `plugin/loader.rb:27`), +but `Plugin#find_command` does `commands[command_name.to_s.to_sym]` (`plugin.rb:38`) — a +Hash-style lookup that raises `TypeError` on an Array. The method is currently dead code, +which is why nothing crashes, but it fails the moment anyone uses it. + +### 8. The bundled `github` plugin cannot work + +Two independent problems in `plugins/github.rb`: + +- `promt.mask(...)` (line 16) — a typo for `prompt`, so `github:setup` crashes at the + password prompt. The same typo is reproduced in the README example (line 149), along + with a second README-only bug (`client.user` where only `github` is in scope). +- The auth flow itself (username/password + `X-GitHub-OTP` header) was removed by GitHub in + November 2020. Even with the typo fixed, setup cannot succeed against the real API. + +### 9. Unknown-command logging always logs a blank name + +In `CLI#run` (`cli.rb:30–34`), when `registry.find` raises `CommandNotFound`, the local +`command` was never assigned, so `log_command_not_found(command)` interpolates `nil` and the +log reads "` not found in registry`". The *user-facing* fallback help is fine; only the log +loses the one fact it exists to record. Fix: log `arguments.first`/the requested name instead. + +## Design flaws and rough edges + +Not single-line bugs, but weaknesses worth deliberate decisions: + +- **`git cmd help ` is documented but not implemented.** The README (line 52) + promises per-command help; the CLI has no `help` command — `git cmd help current` hits + `CommandNotFound` and prints the *global* help. `Command#help` exists and is only reachable + when option parsing fails. +- **Short-flag collisions.** CLI flags/switches auto-derive their short form from the first + letter of the name (`cli.rb:104,112`). Two options starting with the same letter silently + fight over the same `-x`. +- **`exit 1` inside `parse_command_options!`** (`cli.rb:67`) kills the host process from + library code. Anyone embedding the CLI (or testing it) would rather see an exception + bubble to the entry point. +- **Registered commands are stateful.** `CLI#parse_command_options!` writes parsed values + into the `Option` objects owned by the registered `Command`, and `Command#run` merges + them into a hash keyed by name. One-shot CLI runs mask this, but any long-lived process + (REPL, test suite, future daemon) leaks values across runs, and options that share a name + clobber each other. +- **All-or-nothing file loading.** `Registry#load` registers nothing if a file produced any + error, discarding commands in that file that parsed cleanly. Reasonable as a safety + stance, but combined with warning-level logging to a file in `/tmp`, the user experience + is "my command vanished, silently". +- **Repo-local plugins can't be declared as dependencies.** `plugin :name` resolves symbols + against the *bundled* plugin directory only (`plugin/loader.rb:44–46`). Plugins in + `.git-commands/plugins/` load only via the entry-point glob — after command files — + so a command file can't express "I need my repo's custom plugin", and load order is + implicit. Relatedly, plugin file names containing dots truncate (`my.plugin.rb` → `my`). +- **Signature drift between the DSL and the loader.** `Raw#plugin` and `Plugin::Loader#plugin` + forward `**options` into `Plugin::Loader#load(name)`, which accepts none — any options + raise `ArgumentError`. Likewise `Loaders::FileLoader#load` doesn't match its parent's + `load(options = {})` contract. Symptoms of an interface that evolved without a test on + the seam. +- **Inline gemfiles don't install.** `Loader` requires `bundler/inline`, and plugin files + call `gemfile { gem "rugged" }` — but without `gemfile(true)` Bundler only *activates* + already-installed gems. On a machine without `rugged`/`octokit`/`tty-prompt` pre-installed, + every bundled plugin fails to load. First-run experience is broken by default. +- **Duplication that has already drifted.** Help formatting lives in both `CLI#help` and + `Command#help`; `command`/`plugin` DSL methods are near-duplicates between `Raw` and + `Plugin::Loader`; `handle_error` is pasted into three loaders. The two help + implementations already format differently. +- **`System.run` semantics.** `:blocking` actually means "raise on failure" (and only the + literal `false` disables it), `:silent` gates a bare `puts` to process STDOUT — ignoring + the `output` IO abstraction the rest of the codebase carefully threads through. The + docstring ("Supress errors") documents the confusion rather than resolving it. +- **Housekeeping.** Stale `.travis.yml`; CI matrix entry `'3.1.0 '` with trailing space; + `README` still says commands can be organized in "your git-root's directory" while the + entry point only globs `Workflow` and `.git-commands/`; gemspec lacks + `required_ruby_version` and `metadata` (e.g. `rubygems_mfa_required`); `rspec < 4.0` and + `rake ~> 12.3` pins are dated. + +## Security considerations + +These are mostly inherent to the tool's design, but they deserve explicit documentation +and mitigations: + +1. **Running `git cmd` in a repository executes that repository's Ruby code.** The entry + point loads `./Workflow` and `.git-commands/**/*.rb` from the *current working directory* + with no trust prompt. Cloning an untrusted repo and running `git cmd` (even a typo'd + subcommand — loading happens before dispatch) executes attacker-controlled code. Tools + with the same shape (direnv, mise) solve this with an explicit allow/trust step per + directory; git-commander should too. +2. **Inline gemfiles extend that to the gem supply chain.** If auto-install were enabled to + fix the first-run problem, repo-authored code could also install arbitrary gems; any + trust mechanism needs to cover both. +3. **Predictable world-writable log path.** The default `/tmp/git-commander.log` is shared + across users on multi-user systems and is a classic symlink-attack location. Debug-level + logs also capture full command output (`system.rb:37–44`), which can include secrets. + Prefer `~/.local/state/git-commander/` or `XDG_STATE_HOME`. +4. **Shell commands are interpolated strings.** `System.run` takes a single string and the + README encourages interpolating user-supplied options into it + (`"git log #{options[:base_branch]}.."`), so command authors get injection-prone patterns + as the paved road. Supporting argv-array form (`Open3.capture3(*args)`) would give a safe + default. +5. **The `github` plugin's design predates token auth** — see bug #8. Any rewrite should use + device-flow or PAT-based auth and never prompt for passwords. + +## Recommended improvements + +Roughly in order of impact: + +**Correctness (small, high-value fixes)** +1. Fix the five one-line bugs: pass the block through in `Registry#register`, key the + runner's plugin memoization by name, make `Option#value` respect assigned `false`, + implement `Option#hash`, and split `System::Command#name` on whitespace. Each fix is + 1–3 lines plus a regression spec. +2. Fix the `promt` typo (plugin *and* README) and either rewrite or remove the `github` + plugin's password-based setup. +3. Implement `git cmd help [command]` in the CLI (or wire a built-in `help` command into + the registry) so the documented behavior exists; replace `exit 1` with an exception + handled by `exe/git-cmd`. + +**Compatibility & release hygiene** +4. Regenerate `Gemfile.lock`, add `required_ruby_version`, extend CI to current rubies + (3.2–3.4), delete `.travis.yml`, fix the matrix typo. The two Ruby 3.3 spec failures are + encoding-comparison issues in feature specs and should be fixed alongside. +5. Cut an actual 0.1.0 release (or 0.2.0 with the fixes) — the gem is installable from + source only today. + +**Design** +6. Make inline gemfiles work on first run: `gemfile(true)` (with user consent — see + security note), or vendor the bundled plugins' dependencies as proper gem dependencies. +7. Add a per-directory trust mechanism before evaluating repo-local code. +8. Make parsed option values per-invocation (duplicate the command or its options at run + time) instead of mutating registry state. +9. Consolidate the duplicated DSL (`Raw` vs `Plugin::Loader`) and help-text formatting + (`CLI` vs `Command`) into shared modules; unify loader `load` signatures. +10. Let `plugin :name` resolve repo-local plugins (search `.git-commands/plugins/` before + the bundled directory), and document load order. + +**Safety & ergonomics** +11. Support argv-style `system.run("git", "log", ref)` to avoid shell interpolation. +12. Rename/clarify `System.run` options (`raise_on_failure:`, `echo:`), and route its + output through the command's `output` IO. +13. Move the default log file out of `/tmp` and `chomp` the configured path. From 3cd7612dcee575641b376ec60d06c4005c518dab Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 20:26:30 +0000 Subject: [PATCH 2/5] Fix nine bugs found in the repository analysis - Registry#register now passes its block through to the Command, so commands registered via the public API actually run their blocks - Command::Runner memoizes plugin executors per plugin name, so commands referencing multiple plugins no longer all resolve to the first one - Command::Option#value only falls back to the default when unset (nil), so switches with truthy defaults can be turned off with --no- - Command::Option implements #hash to match #eql?, so option Sets dedupe - System::Command#name splits on whitespace instead of word characters, so RunError messages include the failed command's name - Logger chomps the log file path read from git config - Plugin#find_command searches the commands Array instead of indexing it like a Hash - The github plugin's setup command uses personal access token auth (the old username/password + OTP flow was removed by GitHub in 2020) and fixes the 'promt' typo; README example updated to match - CLI logs the requested command name when it isn't found, instead of nil Also implements the README-documented 'git cmd help [command]' subcommand, adds regression specs for each fix, and fixes the two feature-spec failures on modern Rubies by normalizing captured output to UTF-8 in CommandHelpers. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0159sPGBuYwWZLMknPaw6AK4 --- README.md | 21 ++++------ docs/REPO_ANALYSIS.md | 5 +++ lib/git_commander/cli.rb | 24 +++++++++-- lib/git_commander/command.rb | 2 +- lib/git_commander/command/option.rb | 8 +++- lib/git_commander/command/runner.rb | 3 +- lib/git_commander/logger.rb | 2 +- lib/git_commander/plugin.rb | 3 +- lib/git_commander/plugins/github.rb | 21 ++++------ lib/git_commander/registry.rb | 2 +- lib/git_commander/system.rb | 2 +- spec/lib/git_commander/cli_spec.rb | 14 +++++++ spec/lib/git_commander/command/option_spec.rb | 41 +++++++++++++++++++ spec/lib/git_commander/command/runner_spec.rb | 15 +++++++ spec/lib/git_commander/logger_spec.rb | 13 ++++++ spec/lib/git_commander/plugin_spec.rb | 17 ++++++++ spec/lib/git_commander/registry_spec.rb | 9 ++++ spec/lib/git_commander/system_spec.rb | 15 +++++++ spec/support/command_helpers.rb | 9 +++- 19 files changed, 188 insertions(+), 38 deletions(-) create mode 100644 spec/lib/git_commander/command/option_spec.rb diff --git a/README.md b/README.md index 4a8da33..434d681 100644 --- a/README.md +++ b/README.md @@ -142,26 +142,21 @@ end plugin :prompt command :setup do |cmd| - cmd.summary "Connects to GitHub, creates an access token, and stores it in the git-cmd section of your git config" + cmd.summary "Connects to GitHub using a personal access token and verifies the credentials" cmd.on_run do - gh_user = prompt.ask("Please enter your GitHub username", required: true) - gh_password = promt.mask("Please enter your GitHub password (this is NOT stored): ", required: true) + gh_token = prompt.mask( + "Please enter a GitHub personal access token (https://github.com/settings/tokens): ", + required: true + ) - github.login = gh_user - github.password = gh_password + github.access_token = gh_token - # Check for 2-factor requirements begin - client.user + say "GitHub account @#{github.user.login} successfully setup!" rescue Octokit::Unauthorized - github.user( - gh_user, - headers: { "X-GitHub-OTP" => prompt.ask("Please enter your two-factor authentication code") } - ) + say "GitHub could not authenticate with the provided token. Please check the token and try again." end - - say "GitHub account successfully setup!" end end diff --git a/docs/REPO_ANALYSIS.md b/docs/REPO_ANALYSIS.md index 49f6778..328b72f 100644 --- a/docs/REPO_ANALYSIS.md +++ b/docs/REPO_ANALYSIS.md @@ -3,6 +3,11 @@ *An in-depth review of how this codebase works, the value it provides, how it is designed, and where it has flaws or room to improve.* +> **Status update:** the nine confirmed bugs below, the missing `git cmd help [command]` +> subcommand, the dependency/lockfile incompatibility with modern Ruby, and the stale CI +> configuration have all been fixed on this branch (with regression specs). The findings are +> preserved here as documentation of the analysis against `main` @ `50c0f9f`. + - [What this project is](#what-this-project-is) - [The value it provides](#the-value-it-provides) - [How it works](#how-it-works) diff --git a/lib/git_commander/cli.rb b/lib/git_commander/cli.rb index 29ccb7e..0a52f38 100644 --- a/lib/git_commander/cli.rb +++ b/lib/git_commander/cli.rb @@ -27,11 +27,14 @@ def initialize(registry: GitCommander::Registry.new, output: STDOUT) # @param args [Array] (ARGV) a list of arguments to pass to the registered command def run(args = ARGV) arguments = Array(args) - command = registry.find arguments.shift + command_name = arguments.shift + return run_help(arguments) if command_name.to_s == "help" + + command = registry.find command_name options = parse_command_options!(command, arguments) command.run options rescue Registry::CommandNotFound - log_command_not_found(command) + log_command_not_found(command_name) help rescue StandardError => e @@ -69,6 +72,19 @@ def parse_command_options!(command, arguments) private + # Runs the help command, showing a specific command's help text when a + # command name is given (`git-cmd help my-command`), and the global help + # text otherwise. + def run_help(arguments) + command_name = arguments.shift + return help if command_name.to_s.empty? + + registry.find(command_name).help + rescue Registry::CommandNotFound + log_command_not_found(command_name) + help + end + def help say "NAME" say " git-cmd – Git Commander allows running custom git commands from a centralized location." @@ -80,9 +96,9 @@ def help say registry.commands.keys.join(", ") end - def log_command_not_found(command) + def log_command_not_found(command_name) GitCommander.logger.error <<~ERROR_LOG - #{command} not found in registry. Available commands: #{registry.commands.keys.inspect} + #{command_name.inspect} not found in registry. Available commands: #{registry.commands.keys.inspect} ERROR_LOG end diff --git a/lib/git_commander/command.rb b/lib/git_commander/command.rb index 404229c..810a909 100644 --- a/lib/git_commander/command.rb +++ b/lib/git_commander/command.rb @@ -39,7 +39,7 @@ def initialize(name, options = {}, &block) @name = name @description = options[:description] @summary = options[:summary] - @block = block_given? ? block : proc {} + @block = block || options[:block] || proc {} @registry = options[:registry] || GitCommander::Registry.new @output = options[:output] || $stdout diff --git a/lib/git_commander/command/option.rb b/lib/git_commander/command/option.rb index 439c79e..b1f004c 100644 --- a/lib/git_commander/command/option.rb +++ b/lib/git_commander/command/option.rb @@ -23,8 +23,10 @@ def initialize(name:, default: nil, description: nil, value: nil) @value = value end + # An explicitly assigned +false+ must win over the default, so only a + # +nil+ value (meaning "unset") falls back. def value - @value || @default + @value.nil? ? @default : @value end def ==(other) @@ -35,6 +37,10 @@ def ==(other) end alias eql? == + def hash + [self.class, name, default, description].hash + end + def to_h { name => value } end diff --git a/lib/git_commander/command/runner.rb b/lib/git_commander/command/runner.rb index 76e8315..db978c7 100644 --- a/lib/git_commander/command/runner.rb +++ b/lib/git_commander/command/runner.rb @@ -38,7 +38,8 @@ def method_missing(method_sym, *arguments, &block) private def plugin_executor(plugin_name) - @plugin_executor ||= command.registry.find_plugin(plugin_name)&.executor + @plugin_executors ||= {} + @plugin_executors[plugin_name] ||= command.registry.find_plugin(plugin_name)&.executor end end end diff --git a/lib/git_commander/logger.rb b/lib/git_commander/logger.rb index 6762af8..653c354 100644 --- a/lib/git_commander/logger.rb +++ b/lib/git_commander/logger.rb @@ -29,7 +29,7 @@ def log_file_path # Here we have to run the command in isolation to avoid a recursive loop # to log this command run to fetch the config setting. - configured_log_file_path = `git config --get commander.log-file-path` + configured_log_file_path = `git config --get commander.log-file-path`.chomp return @log_file_path = DEFAULT_LOG_FILE if configured_log_file_path.empty? diff --git a/lib/git_commander/plugin.rb b/lib/git_commander/plugin.rb index a9a4c2b..ab4b0d8 100644 --- a/lib/git_commander/plugin.rb +++ b/lib/git_commander/plugin.rb @@ -35,7 +35,8 @@ def initialize(name, source_instance: nil, registry: nil) def find_command(command_name) GitCommander.logger.debug "[#{logger_tag}] looking up command: #{command_name.inspect}" - command = commands[command_name.to_s.to_sym] + target_name = command_name.to_s.to_sym + command = Array(commands).find { |cmd| cmd.name == target_name } raise CommandNotFound, "[#{logger_tag}] #{command_name} does not exist for this plugin" if command.nil? command diff --git a/lib/git_commander/plugins/github.rb b/lib/git_commander/plugins/github.rb index 31cc55b..daec0a7 100644 --- a/lib/git_commander/plugins/github.rb +++ b/lib/git_commander/plugins/github.rb @@ -9,26 +9,21 @@ plugin :prompt command :setup do |cmd| - cmd.summary "Connects to GitHub, creates an access token, and stores it in the git-cmd section of your git config" + cmd.summary "Connects to GitHub using a personal access token and verifies the credentials" cmd.on_run do - gh_user = prompt.ask("Please enter your GitHub username", required: true) - gh_password = promt.mask("Please enter your GitHub password (this is NOT stored): ", required: true) + gh_token = prompt.mask( + "Please enter a GitHub personal access token (https://github.com/settings/tokens): ", + required: true + ) - github.login = gh_user - github.password = gh_password + github.access_token = gh_token - # Check for 2-factor requirements begin - github.user + say "GitHub account @#{github.user.login} successfully setup!" rescue Octokit::Unauthorized - github.user( - gh_user, - headers: { "X-GitHub-OTP" => prompt.ask("Please enter your two-factor authentication code") } - ) + say "GitHub could not authenticate with the provided token. Please check the token and try again." end - - say "GitHub account successfully setup!" end end diff --git a/lib/git_commander/registry.rb b/lib/git_commander/registry.rb index 812411c..56dcdc6 100644 --- a/lib/git_commander/registry.rb +++ b/lib/git_commander/registry.rb @@ -23,7 +23,7 @@ def initialize # @param [String, Symbol] command_name the name of the command to add to the # registry def register(command_name, **options, &block) - command = GitCommander::Command.new(command_name.to_sym, registry: self, **options.merge(block: block)) + command = GitCommander::Command.new(command_name.to_sym, registry: self, **options, &block) register_command(command) end diff --git a/lib/git_commander/system.rb b/lib/git_commander/system.rb index cf8d3bc..fef9f8e 100644 --- a/lib/git_commander/system.rb +++ b/lib/git_commander/system.rb @@ -16,7 +16,7 @@ class Command attr_reader :name, :options, :command_with_arguments def initialize(command_with_arguments, options = {}) @command_with_arguments = command_with_arguments - @name = command_with_arguments.split(/\w/i).first + @name = command_with_arguments.split.first @options = DEFAULT_RUN_OPTIONS.merge(options) end diff --git a/spec/lib/git_commander/cli_spec.rb b/spec/lib/git_commander/cli_spec.rb index 453d462..c7a9fd5 100644 --- a/spec/lib/git_commander/cli_spec.rb +++ b/spec/lib/git_commander/cli_spec.rb @@ -24,6 +24,20 @@ expect(output).to have_received(:puts).with "VERSION" expect(output).to have_received(:puts).with " #{GitCommander::VERSION}" end + + it "outputs a registered command's help text when given that command's name" do + command = GitCommander::Command.new(:wtf, summary: "Shrugs indifferently", output: output) + registry.register_command command + + cli.run %w[help wtf] + + expect(output).to have_received(:puts).with " git-cmd wtf – Shrugs indifferently" + end + + it "falls back to the global help message when given an unknown command name" do + expect(cli).to receive(:help) + cli.run %w[help nope] + end end it "displays a help message when the given command is not registerd" do diff --git a/spec/lib/git_commander/command/option_spec.rb b/spec/lib/git_commander/command/option_spec.rb new file mode 100644 index 0000000..d957b30 --- /dev/null +++ b/spec/lib/git_commander/command/option_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe GitCommander::Command::Option do + describe "#value" do + it "falls back to the default when no value is set" do + option = described_class.new(name: :answer, default: "Because racecar.") + expect(option.value).to eq "Because racecar." + end + + it "returns the assigned value when one is set" do + option = described_class.new(name: :answer, default: "Because racecar.") + option.value = "Because zombies." + expect(option.value).to eq "Because zombies." + end + + it "prefers an explicitly assigned false over a truthy default" do + option = described_class.new(name: :loud, default: true) + option.value = false + expect(option.value).to be false + end + end + + describe "equality" do + it "treats options with the same name, default, and description as equal" do + option = described_class.new(name: :question, default: "Why?", description: "The question") + twin = described_class.new(name: :question, default: "Why?", description: "The question") + + expect(option).to eql twin + expect(option.hash).to eq twin.hash + end + + it "de-duplicates equal options in a Set" do + option = described_class.new(name: :question) + twin = described_class.new(name: :question) + + expect(Set.new([option, twin]).size).to eq 1 + end + end +end diff --git a/spec/lib/git_commander/command/runner_spec.rb b/spec/lib/git_commander/command/runner_spec.rb index 1315f09..c5af613 100644 --- a/spec/lib/git_commander/command/runner_spec.rb +++ b/spec/lib/git_commander/command/runner_spec.rb @@ -22,4 +22,19 @@ runner.run expect(output).to have_received(:puts).with "I'm on a boat!" end + + it "resolves each referenced plugin to its own executor" do + registry = GitCommander::Registry.new + registry.register_plugin GitCommander::Plugin.new(:alpha, source_instance: "alpha instance") + registry.register_plugin GitCommander::Plugin.new(:beta, source_instance: "beta instance") + command = GitCommander::Command.new(:wtf, output: output, registry: registry) do + say alpha.to_s + say beta.to_s + end + + described_class.new(command).run + + expect(output).to have_received(:puts).with "alpha instance" + expect(output).to have_received(:puts).with "beta instance" + end end diff --git a/spec/lib/git_commander/logger_spec.rb b/spec/lib/git_commander/logger_spec.rb index 7959906..e696cc7 100644 --- a/spec/lib/git_commander/logger_spec.rb +++ b/spec/lib/git_commander/logger_spec.rb @@ -7,4 +7,17 @@ expect(logger.instance_variable_get("@logdev").dev.path).to eq GitCommander::Logger::DEFAULT_LOG_FILE end end + + context "with a log file path configured in git config" do + it "strips the trailing newline from the configured path" do + FileUtils.mkdir_p "tmp" + allow_any_instance_of(described_class).to receive(:`). + with("git config --get commander.log-file-path"). + and_return("tmp/configured.log\n") + + logger = described_class.new + + expect(logger.instance_variable_get("@logdev").dev.path).to eq "tmp/configured.log" + end + end end diff --git a/spec/lib/git_commander/plugin_spec.rb b/spec/lib/git_commander/plugin_spec.rb index 4fc7061..4b96c00 100644 --- a/spec/lib/git_commander/plugin_spec.rb +++ b/spec/lib/git_commander/plugin_spec.rb @@ -8,4 +8,21 @@ expect(described_class::Executor).to receive(:new).with(source_instance) described_class.new :git, source_instance: source_instance end + + describe "#find_command" do + it "finds a loaded command by name" do + plugin = described_class.new(:demo) + command = GitCommander::Command.new(:"demo:setup") + plugin.commands = [command] + + expect(plugin.find_command("demo:setup")).to eq command + end + + it "raises CommandNotFound when the command is not defined by the plugin" do + plugin = described_class.new(:demo) + plugin.commands = [] + + expect { plugin.find_command(:nope) }.to raise_error(described_class::CommandNotFound) + end + end end diff --git a/spec/lib/git_commander/registry_spec.rb b/spec/lib/git_commander/registry_spec.rb index 10af651..f03aefe 100644 --- a/spec/lib/git_commander/registry_spec.rb +++ b/spec/lib/git_commander/registry_spec.rb @@ -70,6 +70,15 @@ expect(wtf_command.description).to eq("WTF is up with x") end + it "passes the given block through to the registered command" do + block_ran = false + registry.register(:wtf) { |_options| block_ran = true } + + registry.find(:wtf).run + + expect(block_ran).to be true + end + it "allows registering pre-built commands" do command = GitCommander::Command.new(:wtf) registry.register_command command diff --git a/spec/lib/git_commander/system_spec.rb b/spec/lib/git_commander/system_spec.rb index 9dfd5c7..383792b 100644 --- a/spec/lib/git_commander/system_spec.rb +++ b/spec/lib/git_commander/system_spec.rb @@ -30,6 +30,15 @@ end end + it "includes the failed command's name in the RunError message" do + run_in_test_context do + capture_io do + expect { described_class.run "ls chowchowracecar" }. + to raise_error(GitCommander::System::RunError, /"ls" failed to run/) + end + end + end + it "returns the output of the command" do run_in_test_context do capture_io do @@ -61,6 +70,12 @@ end end + describe GitCommander::System::Command do + it "parses the command name from the full command string" do + expect(described_class.new("git log --oneline").name).to eq "git" + end + end + describe ".say" do it "adds the provided output to the output stream" do run_in_test_context do diff --git a/spec/support/command_helpers.rb b/spec/support/command_helpers.rb index 1b88f67..4d87aaf 100644 --- a/spec/support/command_helpers.rb +++ b/spec/support/command_helpers.rb @@ -105,9 +105,16 @@ def capture_io end def run_system_call(command_string, fail_on_error: false) - last_command.output, last_command.error, last_command.exit_status = run_in_test_context do + stdout, stderr, status = run_in_test_context do Open3.capture3(command_string) end + # Open3 returns strings in the locale encoding (US-ASCII in minimal CI + # environments), while our expectations are UTF-8 literals. The bytes are + # UTF-8 either way, so relabel them to keep comparisons from raising + # Encoding::CompatibilityError. + last_command.output = stdout.force_encoding(Encoding::UTF_8) + last_command.error = stderr.force_encoding(Encoding::UTF_8) + last_command.exit_status = status.exitstatus return unless fail_on_error && last_command.failed? From 59a158004688f720a35aee75b594e1c9c7c6dc6a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 20:26:31 +0000 Subject: [PATCH 3/5] Upgrade dependencies and regenerate Gemfile.lock - Declare required_ruby_version >= 3.2 - Bump dev dependencies: rake ~> 13.0, rdoc ~> 6.5, rspec ~> 3.12 - Relax the bundler runtime dependency to >= 2.1 - Regenerate Gemfile.lock with Bundler 4 (the old lockfile pinned stringio 3.0.1, which fails to build on Ruby >= 3.2) and add darwin platforms for the macOS CI runners - Update .ruby-version from 3.1.0 to 3.3.6 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0159sPGBuYwWZLMknPaw6AK4 --- .ruby-version | 2 +- Gemfile.lock | 67 +++++++++++++++++++++++++++++-------------- git-commander.gemspec | 10 ++++--- 3 files changed, 53 insertions(+), 26 deletions(-) diff --git a/.ruby-version b/.ruby-version index fd2a018..9c25013 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.1.0 +3.3.6 diff --git a/Gemfile.lock b/Gemfile.lock index 874d2bb..688cd27 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,40 +2,65 @@ PATH remote: . specs: git-commander (0.1.0) - bundler (~> 2.1, >= 1.10.0) + bundler (>= 2.1) GEM remote: https://rubygems.org/ specs: - diff-lcs (1.5.0) - psych (4.0.3) + date (3.5.1) + diff-lcs (1.6.2) + erb (6.0.4) + psych (5.4.0) + date stringio - rake (12.3.3) - rdoc (6.4.0) + rake (13.4.2) + rdoc (6.17.0) + erb psych (>= 4.0.0) - rspec (3.11.0) - rspec-core (~> 3.11.0) - rspec-expectations (~> 3.11.0) - rspec-mocks (~> 3.11.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.0) + tsort + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-support (3.11.0) - stringio (3.0.1) + rspec-support (~> 3.13.0) + rspec-support (3.13.7) + stringio (3.2.0) + tsort (0.2.0) PLATFORMS + arm64-darwin ruby + x86_64-darwin + x86_64-linux DEPENDENCIES git-commander! - rake (~> 12.3) - rdoc (~> 6.2) - rspec (< 4.0) + rake (~> 13.0) + rdoc (~> 6.5) + rspec (~> 3.12) + +CHECKSUMS + date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 + diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 + erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9 + git-commander (0.1.0) + psych (5.4.0) sha256=14f72d69a611af663d7d70e4a7b67d9eb1f3ae9f8d916b478961d5a0075ba5b7 + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rdoc (6.17.0) sha256=0f50d4e568fc98195f9bb155a9e8dff6c7feabfb515fb22ef6df1d12ad5a02b7 + rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587 + rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d + rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 + rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 + rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c + stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f BUNDLED WITH - 2.1.0 + 4.0.9 diff --git a/git-commander.gemspec b/git-commander.gemspec index e6dfe6c..ef8bc56 100644 --- a/git-commander.gemspec +++ b/git-commander.gemspec @@ -22,9 +22,11 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_development_dependency "rake", "~> 12.3" - spec.add_development_dependency "rdoc", "~> 6.2" - spec.add_development_dependency "rspec", "< 4.0" + spec.required_ruby_version = ">= 3.2" - spec.add_dependency "bundler", "~> 2.1", ">= 1.10.0" + spec.add_development_dependency "rake", "~> 13.0" + spec.add_development_dependency "rdoc", "~> 6.5" + spec.add_development_dependency "rspec", "~> 3.12" + + spec.add_dependency "bundler", ">= 2.1" end From 0ffa0518192b88e68e87a7e79549c5828c8111f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 20:26:31 +0000 Subject: [PATCH 4/5] Modernize CI: test against the latest three Rubies - Test matrix now covers Ruby 3.3, 3.4, and 3.5 on Ubuntu and macOS (fixes the stray trailing space in the old '3.1.0 ' entry) with fail-fast disabled so all matrix legs report - Update actions/checkout to v4 - Point CodeQL at the main branch (it targeted the nonexistent master) and upgrade github/codeql-action from the sunset v1 to v3 - Remove the stale Travis CI config Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0159sPGBuYwWZLMknPaw6AK4 --- .github/workflows/codeql-analysis.yml | 36 +++++--------------------- .github/workflows/multi-ruby-tests.yml | 6 ++--- .travis.yml | 5 ---- 3 files changed, 10 insertions(+), 37 deletions(-) delete mode 100644 .travis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0f02fdb..11faabe 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -3,20 +3,14 @@ # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# name: "CodeQL" on: push: - branches: [ master ] + branches: [ main ] pull_request: # The branches below must be a subset of the branches above - branches: [ master ] + branches: [ main ] schedule: - cron: '18 0 * * 1' @@ -33,16 +27,16 @@ jobs: fail-fast: false matrix: language: [ 'ruby' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://git.io/codeql-language-support + # Learn more about CodeQL language support at + # https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning-with-codeql steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,21 +44,5 @@ jobs: # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/multi-ruby-tests.yml b/.github/workflows/multi-ruby-tests.yml index f84e47c..b7366a6 100644 --- a/.github/workflows/multi-ruby-tests.yml +++ b/.github/workflows/multi-ruby-tests.yml @@ -9,13 +9,13 @@ on: jobs: test: strategy: - fail-fast: true + fail-fast: false matrix: os: [ ubuntu-latest, macos-latest ] - ruby: ['2.7.5', '3.0.3', '3.1.0 '] + ruby: [ '3.3', '3.4', '3.5' ] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 57184d7..0000000 --- a/.travis.yml +++ /dev/null @@ -1,5 +0,0 @@ -sudo: false -language: ruby -rvm: - - 2.5.0 -before_install: gem install bundler -v 1.16.2 From abb230688827be236efa8cdefdb9559e92349616 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 19:19:52 +0000 Subject: [PATCH 5/5] Fix CI failures on Ruby 3.4 and 3.5 - Declare logger as a runtime dependency: Ruby 3.5 moved it from default gems to bundled gems, so it must be declared to be loadable under Bundler (this also silences the deprecation warning on 3.4) - Add ostruct to the Gemfile's test group for the plugin loader spec fixture, which is a bundled gem as of Ruby 3.5 for the same reason - Match NoMethodError messages with either quoting style in the raw loader spec: Ruby 3.4 changed `danger!' to 'danger!' Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0159sPGBuYwWZLMknPaw6AK4 --- Gemfile | 3 +++ Gemfile.lock | 6 ++++++ git-commander.gemspec | 2 ++ spec/lib/git_commander/command/loaders/raw_spec.rb | 3 ++- 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 0d7f185..d6f7651 100644 --- a/Gemfile +++ b/Gemfile @@ -4,3 +4,6 @@ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } # Specify your gem's dependencies in git-commander.gemspec gemspec + +# Used by spec fixtures; a bundled gem (no longer a default gem) as of Ruby 3.5 +gem "ostruct", group: :test diff --git a/Gemfile.lock b/Gemfile.lock index 688cd27..c79d5b3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,6 +3,7 @@ PATH specs: git-commander (0.1.0) bundler (>= 2.1) + logger GEM remote: https://rubygems.org/ @@ -10,6 +11,8 @@ GEM date (3.5.1) diff-lcs (1.6.2) erb (6.0.4) + logger (1.7.0) + ostruct (0.6.3) psych (5.4.0) date stringio @@ -42,6 +45,7 @@ PLATFORMS DEPENDENCIES git-commander! + ostruct rake (~> 13.0) rdoc (~> 6.5) rspec (~> 3.12) @@ -51,6 +55,8 @@ CHECKSUMS diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9 git-commander (0.1.0) + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 psych (5.4.0) sha256=14f72d69a611af663d7d70e4a7b67d9eb1f3ae9f8d916b478961d5a0075ba5b7 rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 rdoc (6.17.0) sha256=0f50d4e568fc98195f9bb155a9e8dff6c7feabfb515fb22ef6df1d12ad5a02b7 diff --git a/git-commander.gemspec b/git-commander.gemspec index ef8bc56..fba26ac 100644 --- a/git-commander.gemspec +++ b/git-commander.gemspec @@ -29,4 +29,6 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rspec", "~> 3.12" spec.add_dependency "bundler", ">= 2.1" + # logger is a bundled gem (no longer a default gem) as of Ruby 3.5 + spec.add_dependency "logger" end diff --git a/spec/lib/git_commander/command/loaders/raw_spec.rb b/spec/lib/git_commander/command/loaders/raw_spec.rb index 32fd394..b7bca24 100644 --- a/spec/lib/git_commander/command/loaders/raw_spec.rb +++ b/spec/lib/git_commander/command/loaders/raw_spec.rb @@ -97,7 +97,8 @@ resulting_error = result.errors.first expect(resulting_error).to be_kind_of GitCommander::Command::Configurator::ConfigurationError - expect(resulting_error.message).to include "undefined method \`danger!" + # Ruby 3.4 changed NoMethodError quoting from `danger!' to 'danger!' + expect(resulting_error.message).to match(/undefined method [`']danger!'/) expect(resulting_error.backtrace).to_not be_empty end