diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 542c3afd..1a7fe242 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,19 +37,14 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b # v1.275.0 + - uses: ruby/setup-ruby@8a836efbcebe5de0fe86b48a775b7a31b5c70c93 # v1.277.0 with: ruby-version: '3.4' rubygems: latest bundler: 2 - bundler-cache: false + bundler-cache: true - - name: Run standardrb - run: | - bundle install --jobs 4 --retry 3 - bundle exec standardrb - env: - MAINTENANCE: true + - run: bundle exec standardrb coverage: name: Generate Coverage Report @@ -76,18 +71,14 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b # v1.275.0 + - uses: ruby/setup-ruby@8a836efbcebe5de0fe86b48a775b7a31b5c70c93 # v1.277.0 with: ruby-version: '3.4' rubygems: latest bundler: 2 - bundler-cache: false + bundler-cache: true - - run: | - bundle install --jobs 4 --retry 3 - bundle exec ruby -S rake coverage --trace - env: - COVERAGE: true + - run: bundle exec ruby -S rake coverage --trace - uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b #v2.3.6 @@ -104,12 +95,10 @@ jobs: - ubuntu-22.04 - ubuntu-24.04 ruby: - - '2.6' - - '2.7' - - '3.1' - '3.2' - '3.3' - '3.4' + - '4.0' - truffleruby runs-on: ${{ matrix.os }} @@ -131,7 +120,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b # v1.275.0 + - uses: ruby/setup-ruby@8a836efbcebe5de0fe86b48a775b7a31b5c70c93 # v1.277.0 with: ruby-version: ${{ matrix.ruby }} rubygems: latest @@ -154,12 +143,10 @@ jobs: - macos-15 - macos-26 ruby: - - '2.6' - - '2.7' - - '3.1' - '3.2' - '3.3' - '3.4' + - '4.0' runs-on: ${{ matrix.os }} @@ -180,7 +167,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b # v1.275.0 + - uses: ruby/setup-ruby@8a836efbcebe5de0fe86b48a775b7a31b5c70c93 # v1.277.0 with: ruby-version: ${{ matrix.ruby }} rubygems: latest @@ -202,13 +189,10 @@ jobs: - windows-2022 - windows-2025 ruby: - - '2.6' - - '2.7' - - '3.0' - - '3.1' - '3.2' - '3.3' - '3.4' + - '4.0' - mingw - mswin - ucrt @@ -232,7 +216,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b # v1.275.0 + - uses: ruby/setup-ruby@8a836efbcebe5de0fe86b48a775b7a31b5c70c93 # v1.277.0 with: ruby-version: ${{ matrix.ruby }} rubygems: latest @@ -278,7 +262,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b # v1.275.0 + - uses: ruby/setup-ruby@8a836efbcebe5de0fe86b48a775b7a31b5c70c93 # v1.277.0 with: ruby-version: ${{ matrix.ruby }} rubygems: latest @@ -324,7 +308,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b # v1.275.0 + - uses: ruby/setup-ruby@8a836efbcebe5de0fe86b48a775b7a31b5c70c93 # v1.277.0 with: ruby-version: ${{ matrix.ruby }} rubygems: latest diff --git a/.github/workflows/dco-check.yml b/.github/workflows/dco-check.yml index bbb7584f..821f07c2 100644 --- a/.github/workflows/dco-check.yml +++ b/.github/workflows/dco-check.yml @@ -27,4 +27,4 @@ jobs: api.github.com:443 github.com:443 - - uses: KineticCafe/actions-dco@cd9508e5ae82413fbd74b20af21551db0ea3eb78 # v2.0.0 + - uses: KineticCafe/actions-dco@76b7fc30ff5988e68d01ea07deeaf7e71256598f # v2.1.0 diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 53c6c319..fbd62612 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -38,7 +38,7 @@ jobs: 'print "version=", Gem::Specification.load(ARGV[0]).rubygems_version, "\n"' \ diff-lcs.gemspec >>"${GITHUB_OUTPUT}" - - uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b # v1.275.0 + - uses: ruby/setup-ruby@8a836efbcebe5de0fe86b48a775b7a31b5c70c93 # v1.277.0 with: bundler-cache: false ruby-version: ruby diff --git a/.github/workflows/publish-gem.yml b/.github/workflows/publish-gem.yml index fd3f24f4..15492445 100644 --- a/.github/workflows/publish-gem.yml +++ b/.github/workflows/publish-gem.yml @@ -65,7 +65,7 @@ jobs: 'print "version=", Gem::Specification.load(ARGV[0]).rubygems_version, "\n"' \ diff-lcs.gemspec >>"${GITHUB_OUTPUT}" - - uses: ruby/setup-ruby@d354de180d0c9e813cfddfcbdc079945d4be589b # v1.275.0 + - uses: ruby/setup-ruby@8a836efbcebe5de0fe86b48a775b7a31b5c70c93 # v1.277.0 with: bundler-cache: false ruby-version: ruby diff --git a/.mise.toml b/.mise.toml index 43c30961..becf7706 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,6 +1,2 @@ [tools] ruby = "3.4" - -[env] -MAINTENANCE = "true" -COVERAGE = "true" diff --git a/.standard.yml b/.standard.yml index 4aa2233e..982c299f 100644 --- a/.standard.yml +++ b/.standard.yml @@ -1,15 +1,11 @@ --- parallel: true -ruby_version: 2.0 +ruby_version: 3.2 ignore: - '*.gemspec' - research/**/* - pkg/**/* - - Gemfile: - - Style/HashSyntax - - Rakefile: - - Layout/HeredocIndentation plugins: - rubocop-thread_safety diff --git a/CHANGELOG.md b/CHANGELOG.md index c2881230..26af8ea4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Changelog +## 2.0.0.beta.1 / 2025-12-31 + +This release has significant **breaking changes**. + +- diff-lcs 2 supports Ruby 3.2 or higher. This allowed: + + - readability improvements (endless methods, pattern matching); + + - support for immutable Data classes (`Diff::LCS::Block`, `Diff::LCS::Change`, + and `Diff::LCS::ContextChange`); + + - removal of compatibility shims; + + - reduction in conditional tests in loops, especially for String character + extraction (compare `string ? seq[i, 1] : seq[i]` to `seq[i]`); and + + - optimizations to string and relying on standard encoding support present + since Ruby 2.1. + + The primary API (methods and class methods on `Diff::LCS`) has not changed, + with one exception noted below. Internal APIs (including `Diff::LCS::Block`, + `Diff::LCS::Change`, `Diff::LCS::ContextChange`, and `Diff::LCS::Hunk`) have + changed for compatibility. + +- The `htmldiff` binary and supporting code have been removed without + replacement. + +- `ldiff` no longer supports `ed`-script output (`ed` and `reverse_ed` formats). + As Baptiste Courtois says in [#108][pull-108], the "implementation was broken + for a while and no-one reported it." + +- The method `Diff::LCS.LCS` has been removed as an alias for `Diff::LCS.lcs`. + Most callers are using `Diff::LCS.lcs` and modern Ruby did-you-mean support + should assist with this update. + ## 1.6.2 / 2025-05-12 - Handle upcoming changes to the `cgi` gem in Ruby 3.5 ([#147][pull-147]) @@ -504,6 +539,7 @@ [pull-103]: https://github.com/halostatue/diff-lcs/pull/103 [pull-104]: https://github.com/halostatue/diff-lcs/pull/104 [pull-105]: https://github.com/halostatue/diff-lcs/pull/105 +[pull-108]: https://github.com/halostatue/diff-lcs/pull/108 [pull-129]: https://github.com/halostatue/diff-lcs/pull/129 [pull-147]: https://github.com/halostatue/diff-lcs/pull/147 [pull-148]: https://github.com/halostatue/diff-lcs/pull/148 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b8871dc4..785bd0af 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,54 +1,65 @@ # Contributing Contribution to diff-lcs is encouraged: bug reports, feature requests, or code -contributions. There are a few DOs and DON'Ts that should be followed. +contributions. New features should be proposed and discussed in an +[issue][issues]. -## DO +Before contributing patches, please read the [Licence](./LICENCE.md). -- Keep the coding style that already exists for any updated Ruby code (support - or otherwise). I use [Standard Ruby][standardrb] for linting and formatting. +diff-lcs is governed under the [Contributor Covenant Code of Conduct][cccoc]. -- Use thoughtfully-named topic branches for contributions. Rebase your commits - into logical chunks as necessary. +## Code Guidelines -- Use [quality commit messages][qcm]. +I have several guidelines to contributing code through pull requests: -- Add your name or GitHub handle to `CONTRIBUTORS.md` and a record in the - `CHANGELOG.md` as a separate commit from your main change. (Follow the style - in the `CHANGELOG.md` and provide a link to your PR.) +- All code changes require tests. In most cases, this will be added or updated + unit tests. I use [RSpec][rspec]. -- Add or update tests as appropriate for your change. The test suite is written - in [RSpec][rspec]. +- I use code formatters, static analysis tools, and linting to ensure consistent + styles and formatting. There should be no warning output from test run + processes. I use [Standard Ruby][standardrb]. -- Add or update documentation as appropriate for your change. The documentation - is RDoc; diff-lcs does not use extensions that may be present in alternative - documentation generators. +- Proposed changes should be on a thoughtfully-named topic branch and organized + into logical commit chunks as appropriate. -## DO NOT +- Use [Conventional Commits][conventional] with my + [conventions](#commit-conventions). -- Modify `VERSION` in `lib/diff/lcs/version.rb`. When your patch is accepted and - a release is made, the version will be updated at that point. +- Versions must not be updated in pull requests unless otherwise directed. This + means that you must not: -- Modify `diff-lcs.gemspec`; it is a generated file. (You _may_ use - `rake gemspec` to regenerate it if your change involves metadata related to - gem itself). + - Modify `VERSION` in `lib/diff/lcs/version.rb`. When your patch is accepted + and a release is made, the version will be updated at that point. -- Modify the `Gemfile`. + - Modify `diff-lcs.gemspec`; it is a generated file. (You _may_ use + `rake gemspec` to regenerate it if your change involves metadata related to + gem itself). -## LLM-Generated Contribution Policy + - Modify the `Gemfile`. -diff-lcs is a library with complex interactions and subtle decisions (some of -them possibly even wrong). It is extremely important that any issues or pull -requests be well understood by the submitter and that, especially for pull -requests, the developer can attest to the [Developer Certificate of Origin][dco] -for each pull request (see [LICENCE](LICENCE.md)). +- Documentation should be added or updated as appropriate for new or updated + functionality. The documentation is RDoc; diff-lcs does not use extensions + that may be present in alternative documentation generators. + +- All GitHub Actions checks marked as required must pass before a pull request + may be accepted and merged. + +- Add your name or GitHub handle to `CONTRIBUTORS.md` and a record in the + `CHANGELOG.md` as a separate commit from your main change. (Follow the style + in the `CHANGELOG.md` and provide a link to your PR.) -If LLM assistance is used in writing pull requests, this must be documented in -the commit message and pull request. If there is evidence of LLM assistance -without such declaration, the pull request **will be declined**. +- Include your DCO sign-off in each commit message (see [LICENCE](LICENCE.md)). -Any contribution (bug, feature request, or pull request) that uses unreviewed -LLM output will be rejected. +## AI Contribution Policy + +diff-lcs is a library with complex interactions and subtle decisions (some of +them possibly even wrong). It is extremely important that contributions of any +sort be well understood by the submitter and that the developer can attest to +the [Developer Certificate of Origin][dco] for each pull request (see +[LICENCE](LICENCE.md)). + +Any contribution (bug, feature request, or pull request) that uses undeclared AI +output will be rejected. ## Test Dependencies @@ -58,28 +69,54 @@ tests in the same way that `rake spec` does. To assist with the installation of the development dependencies for diff-lcs, I have provided a Gemfile pointing to the (generated) `diff-lcs.gemspec` file. -`minitar.gemspec` file. This will permit you to use `bundle install` to install -the dependencies. +This will permit you to use `bundle install` to install the dependencies. You can run tests with code coverage analysis by running `rake spec:coverage`. -## Workflow +## Commit Conventions + +diff-lcs has adopted a variation of the Conventional Commits format for commit +messages. The following types are permitted: + +| Type | Purpose | +| ------- | ----------------------------------------------------- | +| `feat` | A new feature | +| `fix` | A bug fix | +| `chore` | A code change that is neither a bug fix nor a feature | +| `docs` | Documentation updates | +| `deps` | Dependency updates, including GitHub Actions. | + +I encourage the use of [Tim Pope's][tpope-qcm] or [Chris Beam's][cbeams] +guidelines on the writing of commit messages + +I require the use of [git][trailers1] [trailers][trailers2] for specific +additional metadata and strongly encourage it for others. The conditionally +required metadata trailers are: + +- `Breaking-Change`: if the change is a breaking change. **Do not** use the + shorthand form (`feat!(scope)`) or `BREAKING CHANGE`. + +- `Signed-off-by`: this is required for all developers except me, as outlined in + the [Licence](./LICENCE.md#developer-certificate-of-origin). -Here's the most direct way to get your work merged into the project: +- `Fixes` or `Resolves`: If a change fixes one or more open [issues][issues], + that issue must be included in the `Fixes` or `Resolves` trailer. Multiple + issues should be listed comma separated in the same trailer: + `Fixes: #1, #5, #7`, but _may_ appear in separate trailers. While both `Fixes` + and `Resolves` are synonyms, only _one_ should be used in a given commit or + pull request. -- Fork the project. -- Clone your fork (`git clone git://github.com//diff-lcs.git`). -- Create a topic branch to contain your change - (`git checkout -b my_awesome_feature`). -- Hack away, add tests. Not necessarily in that order. -- Make sure everything still passes by running `rake`. -- If necessary, rebase your commits into logical chunks, without errors. -- Push the branch up (`git push origin my_awesome_feature`). -- Create a pull request against halostatue/diff-lcs and describe what your - change does and the why you think it should be merged. +- `Related to`: If a change does not fix an issue, those issue references should + be included in this trailer. +[cbeams]: https://cbea.ms/git-commit/ +[cccoc]: ./CODE_OF_CONDUCT.md +[conventional]: https://www.conventionalcommits.org/en/v1.0.0/ [dco]: licences/dco.txt [hoe]: https://github.com/seattlerb/hoe -[qcm]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html +[issues]: https://github.com/halostatue/diff-lcs/issues [rspec]: http://rspec.info/documentation/ [standardrb]: https://github.com/standardrb/standard +[tpope-qcm]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html +[trailers1]: https://git-scm.com/docs/git-interpret-trailers +[trailers2]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---trailerlttokengtltvaluegt diff --git a/Gemfile b/Gemfile index d3732979..deb729f8 100644 --- a/Gemfile +++ b/Gemfile @@ -1,21 +1,6 @@ -# NOTE: This file is not the canonical source of dependencies. Edit the -# Rakefile, instead. - +# NOTE: This file is not the canonical source of dependencies. Edit the Rakefile, instead. source "https://rubygems.org/" -if ENV["DEV"] - gem "debug", :platforms => [:mri] -end - -if ENV["COVERAGE"] == "true" - gem "simplecov", :require => false, :platforms => [:mri_34] - gem "simplecov-lcov", :require => false, :platforms => [:mri_34] -end - -if ENV["MAINTENANCE"] == "true" - gem "standard", :require => false, :platforms => [:mri_34] - gem "standard-thread_safety", :require => false, :platforms => [:mri_34] - gem "fasterer", :require => false, :platforms => [:mri_34] -end +gem "debug", platforms: [:mri] if ENV["DEV"] gemspec diff --git a/LICENCE.md b/LICENCE.md index 12c22bfa..5a2f1b27 100644 --- a/LICENCE.md +++ b/LICENCE.md @@ -11,7 +11,7 @@ GNU GPL ("the same terms as Perl itself") and given that the Ruby implementation originally hewed pretty closely to the Perl version, I must maintain the additional licensing terms. -- Copyright 2004-2025 Austin Ziegler and contributors. +- Copyright 2004-2026 Austin Ziegler and contributors. - Adapted from Algorithm::Diff (Perl) by Ned Konz and a Smalltalk version by Mario I. Wolczko. diff --git a/Manifest.txt b/Manifest.txt index d9f675a0..8fbb5c71 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -1,4 +1,3 @@ -.rspec CHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md @@ -8,16 +7,13 @@ Manifest.txt README.md Rakefile SECURITY.md -bin/htmldiff bin/ldiff lib/diff-lcs.rb lib/diff/lcs.rb lib/diff/lcs/array.rb -lib/diff/lcs/backports.rb lib/diff/lcs/block.rb lib/diff/lcs/callbacks.rb lib/diff/lcs/change.rb -lib/diff/lcs/htmldiff.rb lib/diff/lcs/hunk.rb lib/diff/lcs/internals.rb lib/diff/lcs/ldiff.rb @@ -51,8 +47,6 @@ spec/fixtures/ldiff/error.diff.missing_new_line2-e spec/fixtures/ldiff/error.diff.missing_new_line2-f spec/fixtures/ldiff/output.diff spec/fixtures/ldiff/output.diff-c -spec/fixtures/ldiff/output.diff-e -spec/fixtures/ldiff/output.diff-f spec/fixtures/ldiff/output.diff-u spec/fixtures/ldiff/output.diff.bin1 spec/fixtures/ldiff/output.diff.bin1-c @@ -66,14 +60,10 @@ spec/fixtures/ldiff/output.diff.bin2-f spec/fixtures/ldiff/output.diff.bin2-u spec/fixtures/ldiff/output.diff.chef spec/fixtures/ldiff/output.diff.chef-c -spec/fixtures/ldiff/output.diff.chef-e -spec/fixtures/ldiff/output.diff.chef-f spec/fixtures/ldiff/output.diff.chef-u spec/fixtures/ldiff/output.diff.chef2 spec/fixtures/ldiff/output.diff.chef2-c spec/fixtures/ldiff/output.diff.chef2-d -spec/fixtures/ldiff/output.diff.chef2-e -spec/fixtures/ldiff/output.diff.chef2-f spec/fixtures/ldiff/output.diff.chef2-u spec/fixtures/ldiff/output.diff.empty.vs.four_lines spec/fixtures/ldiff/output.diff.empty.vs.four_lines-c diff --git a/Rakefile b/Rakefile index 941fb161..b9da24b5 100644 --- a/Rakefile +++ b/Rakefile @@ -13,34 +13,12 @@ Hoe.plugins.delete :newb Hoe.plugins.delete :publish Hoe.plugins.delete :signing -if RUBY_VERSION < "1.9" - class Array # :nodoc: - def to_h - Hash[*flatten(1)] - end - end - - class Gem::Specification # :nodoc: - def metadata=(*) - end - - def default_value(*) - end - end - - class Object # :nodoc: - def caller_locations(*) - [] - end - end -end - hoe = Hoe.spec "diff-lcs" do developer("Austin Ziegler", "halostatue@gmail.com") self.trusted_release = ENV["rubygems_release_gem"] == "true" - require_ruby_version ">= 1.8" + require_ruby_version ">= 3.2.0", "< 5" self.licenses = ["MIT", "Artistic-1.0-Perl", "GPL-2.0-or-later"] @@ -50,10 +28,14 @@ hoe = Hoe.spec "diff-lcs" do extra_dev_deps << ["hoe", "~> 4.0"] extra_dev_deps << ["hoe-halostatue", "~> 2.1", ">= 2.1.1"] - extra_dev_deps << ["hoe-git", "~> 1.6"] extra_dev_deps << ["rspec", ">= 2.0", "< 4"] extra_dev_deps << ["rake", ">= 10.0", "< 14"] extra_dev_deps << ["rdoc", ">= 6.3.1", "< 7"] + extra_dev_deps << ["simplecov", "~> 0.9"] + extra_dev_deps << ["simplecov-lcov", "~> 0.9"] + extra_dev_deps << ["standard", "~> 1.50"] + extra_dev_deps << ["standard-thread_safety", "~> 1.0"] + extra_dev_deps << ["fasterer", "~> 0.11"] end desc "Run all specifications" @@ -61,22 +43,19 @@ RSpec::Core::RakeTask.new(:spec) do |t| rspec_dirs = %w[spec lib].join(":") t.rspec_opts = ["-I#{rspec_dirs}"] end -if RUBY_VERSION >= "3.0" && RUBY_ENGINE == "ruby" - namespace :spec do - desc "Runs test coverage. Only works Ruby 2.0+ and assumes 'simplecov' is installed." - task :coverage do - ENV["COVERAGE"] = "true" - Rake::Task["spec"].execute - end - end - task coverage: "spec:coverage" +namespace :spec do + desc "Runs test coverage. Only works Ruby 2.0+ and assumes 'simplecov' is installed." + task :coverage do + Rake::Task["spec"].execute + end end + +task coverage: "spec:coverage" Rake::Task["spec"].actions.uniq! { |a| a.source_location } -# standard:disable Style/HashSyntax -task :default => :spec unless Rake::Task["default"].prereqs.include?("spec") -task :test => :spec unless Rake::Task["test"].prereqs.include?("spec") -# standard:enable Style/HashSyntax + +task default: :spec unless Rake::Task["default"].prereqs.include?("spec") +task test: :spec unless Rake::Task["test"].prereqs.include?("spec") task :version do require "diff/lcs/version" @@ -89,23 +68,6 @@ RDoc::Task.new do |config| config.main = "README.md" config.rdoc_dir = "doc" config.rdoc_files = hoe.spec.require_paths - ["Manifest.txt"] + hoe.spec.extra_rdoc_files - # config.markup = "markdown" + config.markup = "markdown" end task docs: :rerdoc - -if ENV["MAINTENANCE"] == "true" - task ruby18: :package do - require "diff/lcs/version" - # standard:disable Layout/HeredocIndentation - puts <<-MESSAGE -You are starting a barebones Ruby 1.8 docker environment for testing. -A snapshot package has been built, so install it with: - - cd diff-lcs - gem install pkg/diff-lcs-#{Diff::LCS::VERSION} - - MESSAGE - # standard:enable Layout/HeredocIndentation - sh "docker run -it --rm -v #{Dir.pwd}:/root/diff-lcs bellbind/docker-ruby18-rails2 bash -l" - end -end diff --git a/SECURITY.md b/SECURITY.md index 3c61c1b1..6bd704b9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,41 +7,39 @@ agents. ## Supported Versions -Security reports are accepted for the most recent major release and the previous -version for a limited time after the initial major release version. After a -major release, the previous version will receive full support for six months and -security support for an additional six months (for a total of twelve months). +Security reports are accepted for the most recent major release, with a limited +window of support after the initial major release. -Because diff-lcs 1.x supports a wide range of Ruby versions that are themselves -end of life, security reports will only be accepted when they can be -demonstrated on Ruby 3.2 or higher. +- Bug reports will be accepted up to three months after release. +- Security reports will be accepted up to six months after release. -> [!information] -> -> There will be a diff-lcs 2.0 released in 2025 which narrows support to modern -> versions of Ruby only. +All issues raised must be demonstrated on the minimum supported Ruby version. + +> [!important] > -> | Release Date | Support Ends | Security Support Ends | -> | ------------ | ------------ | --------------------- | -> | 2025 | +6 months | +12 months | +> Because diff-lcs 1 has been the only version for over twenty years, security +> reports will be accepted for one year after the release of diff-lcs 2. > -> If the 2.0.0 release happens on 2025-08-01, regular support for diff-lcs 1.x -> will end on 2026-01-31 and security support for diff-lcs 1.x will end on -> 2026-07-31. +> | Version | Release Date | Support Ends | Security Support Ends | +> | ------- | ------------ | ------------ | --------------------- | +> | 1.x | 2010 | 2026-04-30 | 2027-01-31 | +> | 2.x | 2026-01-31 | - | - | ## Reporting a Vulnerability -By preference, use the [Tidelift security contact][tidelift]. Tidelift will -coordinate the fix and disclosure. +Report vulnerabilities via the [Tidelift security contact][tidelift]. Tidelift +will coordinate the fix and disclosure. -Alternatively, Send an email to [diff-lcs@halostatue.ca][email] with the text -`Diff::LCS` in the subject. Emails sent to this address should be encrypted -using [age][age] with the following public key: +Alternatively, create a [private vulnerability report][advisory] with GitHub or +send an email to [security@ruby.halostatue.ca][email] with the text `diff-lcs` +in the subject. Emails sent to this address should be encrypted using [age][age] +with the following public key: ``` age1fc6ngxmn02m62fej5cl30lrvwmxn4k3q2atqu53aatekmnqfwumqj4g93w ``` -[tidelift]: https://tidelift.com/security -[email]: mailto:diff-lcs@halostatue.ca +[advisory]: https://github.com/halostatue/diff-lcs/security/advisories/new [age]: https://github.com/FiloSottile/age +[email]: mailto:security@ruby.halostatue.ca +[tidelift]: https://tidelift.com/security diff --git a/bin/htmldiff b/bin/htmldiff deleted file mode 100755 index bcd89d21..00000000 --- a/bin/htmldiff +++ /dev/null @@ -1,35 +0,0 @@ -#! /usr/bin/env ruby -w -# frozen_string_literal: true - -require "diff/lcs" -require "diff/lcs/htmldiff" - -begin - require "text/format" -rescue LoadError - Diff::LCS::HTMLDiff.can_expand_tabs = false -end - -if ARGV.size < 2 or ARGV.size > 3 - warn "usage: #{File.basename($0)} old new [output.html]" - warn " #{File.basename($0)} old new > output.html" - exit 127 -end - -left = IO.read(ARGV[0]).split($/) -right = IO.read(ARGV[1]).split($/) - -options = { :title => "diff #{ARGV[0]} #{ARGV[1]}" } - -htmldiff = Diff::LCS::HTMLDiff.new(left, right, options) - -if ARGV[2] - File.open(ARGV[2], "w") do |f| - htmldiff.options[:output] = f - htmldiff.run - end -else - htmldiff.run -end - -# vim: ft=ruby diff --git a/diff-lcs.gemspec b/diff-lcs.gemspec index ed9550ee..32f58556 100644 --- a/diff-lcs.gemspec +++ b/diff-lcs.gemspec @@ -1,24 +1,24 @@ # -*- encoding: utf-8 -*- -# stub: diff-lcs 1.6.2 ruby lib +# stub: diff-lcs 2.0.0.beta.1 ruby lib Gem::Specification.new do |s| s.name = "diff-lcs".freeze - s.version = "1.6.2".freeze + s.version = "2.0.0.beta.1".freeze s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.metadata = { "bug_tracker_uri" => "https://github.com/halostatue/diff-lcs/issues", "changelog_uri" => "https://github.com/halostatue/diff-lcs/blob/main/CHANGELOG.md", "homepage_uri" => "https://github.com/halostatue/diff-lcs", "rubygems_mfa_required" => "true", "source_code_uri" => "https://github.com/halostatue/diff-lcs" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["Austin Ziegler".freeze] - s.date = "1980-01-02" - s.description = "Diff::LCS computes the difference between two Enumerable sequences using the\nMcIlroy-Hunt longest common subsequence (LCS) algorithm. It includes utilities\nto create a simple HTML diff output format and a standard diff-like tool.\n\nThis is release 1.6.1, providing a simple extension that allows for\nDiff::LCS::Change objects to be treated implicitly as arrays and fixes a number\nof formatting issues.\n\nRuby versions below 2.5 are soft-deprecated, which means that older versions are\nno longer part of the CI test suite. If any changes have been introduced that\nbreak those versions, bug reports and patches will be accepted, but it will be\nup to the reporter to verify any fixes prior to release. The next major release\nwill completely break compatibility.".freeze + s.date = "2025-12-31" + s.description = "Diff::LCS computes the difference between two Enumerable sequences using the McIlroy-Hunt longest common subsequence (LCS) algorithm. It includes utilities to create a simple HTML diff output format and a standard diff-like tool. This is release 1.6.1, providing a simple extension that allows for Diff::LCS::Change objects to be treated implicitly as arrays and fixes a number of formatting issues. Ruby versions below 2.5 are soft-deprecated, which means that older versions are no longer part of the CI test suite. If any changes have been introduced that break those versions, bug reports and patches will be accepted, but it will be up to the reporter to verify any fixes prior to release. The next major release will completely break compatibility.".freeze s.email = ["halostatue@gmail.com".freeze] - s.executables = ["htmldiff".freeze, "ldiff".freeze] + s.executables = ["ldiff".freeze] s.extra_rdoc_files = ["CHANGELOG.md".freeze, "CODE_OF_CONDUCT.md".freeze, "CONTRIBUTING.md".freeze, "CONTRIBUTORS.md".freeze, "LICENCE.md".freeze, "Manifest.txt".freeze, "README.md".freeze, "SECURITY.md".freeze, "licenses/COPYING.txt".freeze, "licenses/artistic.txt".freeze, "licenses/dco.txt".freeze] - s.files = [".rspec".freeze, "CHANGELOG.md".freeze, "CODE_OF_CONDUCT.md".freeze, "CONTRIBUTING.md".freeze, "CONTRIBUTORS.md".freeze, "LICENCE.md".freeze, "Manifest.txt".freeze, "README.md".freeze, "Rakefile".freeze, "SECURITY.md".freeze, "bin/htmldiff".freeze, "bin/ldiff".freeze, "lib/diff-lcs.rb".freeze, "lib/diff/lcs.rb".freeze, "lib/diff/lcs/array.rb".freeze, "lib/diff/lcs/backports.rb".freeze, "lib/diff/lcs/block.rb".freeze, "lib/diff/lcs/callbacks.rb".freeze, "lib/diff/lcs/change.rb".freeze, "lib/diff/lcs/htmldiff.rb".freeze, "lib/diff/lcs/hunk.rb".freeze, "lib/diff/lcs/internals.rb".freeze, "lib/diff/lcs/ldiff.rb".freeze, "lib/diff/lcs/string.rb".freeze, "lib/diff/lcs/version.rb".freeze, "licenses/COPYING.txt".freeze, "licenses/artistic.txt".freeze, "licenses/dco.txt".freeze, "spec/change_spec.rb".freeze, "spec/diff_spec.rb".freeze, "spec/fixtures/123_x".freeze, "spec/fixtures/456_x".freeze, "spec/fixtures/aX".freeze, "spec/fixtures/bXaX".freeze, "spec/fixtures/ds1.csv".freeze, "spec/fixtures/ds2.csv".freeze, "spec/fixtures/empty".freeze, "spec/fixtures/file1.bin".freeze, "spec/fixtures/file2.bin".freeze, "spec/fixtures/four_lines".freeze, "spec/fixtures/four_lines_with_missing_new_line".freeze, "spec/fixtures/ldiff/diff.missing_new_line1-e".freeze, "spec/fixtures/ldiff/diff.missing_new_line1-f".freeze, "spec/fixtures/ldiff/diff.missing_new_line2-e".freeze, "spec/fixtures/ldiff/diff.missing_new_line2-f".freeze, "spec/fixtures/ldiff/error.diff.chef-e".freeze, "spec/fixtures/ldiff/error.diff.chef-f".freeze, "spec/fixtures/ldiff/error.diff.missing_new_line1-e".freeze, "spec/fixtures/ldiff/error.diff.missing_new_line1-f".freeze, "spec/fixtures/ldiff/error.diff.missing_new_line2-e".freeze, "spec/fixtures/ldiff/error.diff.missing_new_line2-f".freeze, "spec/fixtures/ldiff/output.diff".freeze, "spec/fixtures/ldiff/output.diff-c".freeze, "spec/fixtures/ldiff/output.diff-e".freeze, "spec/fixtures/ldiff/output.diff-f".freeze, "spec/fixtures/ldiff/output.diff-u".freeze, "spec/fixtures/ldiff/output.diff.bin1".freeze, "spec/fixtures/ldiff/output.diff.bin1-c".freeze, "spec/fixtures/ldiff/output.diff.bin1-e".freeze, "spec/fixtures/ldiff/output.diff.bin1-f".freeze, "spec/fixtures/ldiff/output.diff.bin1-u".freeze, "spec/fixtures/ldiff/output.diff.bin2".freeze, "spec/fixtures/ldiff/output.diff.bin2-c".freeze, "spec/fixtures/ldiff/output.diff.bin2-e".freeze, "spec/fixtures/ldiff/output.diff.bin2-f".freeze, "spec/fixtures/ldiff/output.diff.bin2-u".freeze, "spec/fixtures/ldiff/output.diff.chef".freeze, "spec/fixtures/ldiff/output.diff.chef-c".freeze, "spec/fixtures/ldiff/output.diff.chef-e".freeze, "spec/fixtures/ldiff/output.diff.chef-f".freeze, "spec/fixtures/ldiff/output.diff.chef-u".freeze, "spec/fixtures/ldiff/output.diff.chef2".freeze, "spec/fixtures/ldiff/output.diff.chef2-c".freeze, "spec/fixtures/ldiff/output.diff.chef2-d".freeze, "spec/fixtures/ldiff/output.diff.chef2-e".freeze, "spec/fixtures/ldiff/output.diff.chef2-f".freeze, "spec/fixtures/ldiff/output.diff.chef2-u".freeze, "spec/fixtures/ldiff/output.diff.empty.vs.four_lines".freeze, "spec/fixtures/ldiff/output.diff.empty.vs.four_lines-c".freeze, "spec/fixtures/ldiff/output.diff.empty.vs.four_lines-e".freeze, "spec/fixtures/ldiff/output.diff.empty.vs.four_lines-f".freeze, "spec/fixtures/ldiff/output.diff.empty.vs.four_lines-u".freeze, "spec/fixtures/ldiff/output.diff.four_lines.vs.empty".freeze, "spec/fixtures/ldiff/output.diff.four_lines.vs.empty-c".freeze, "spec/fixtures/ldiff/output.diff.four_lines.vs.empty-e".freeze, "spec/fixtures/ldiff/output.diff.four_lines.vs.empty-f".freeze, "spec/fixtures/ldiff/output.diff.four_lines.vs.empty-u".freeze, "spec/fixtures/ldiff/output.diff.issue95_trailing_context".freeze, "spec/fixtures/ldiff/output.diff.issue95_trailing_context-c".freeze, "spec/fixtures/ldiff/output.diff.issue95_trailing_context-e".freeze, "spec/fixtures/ldiff/output.diff.issue95_trailing_context-f".freeze, "spec/fixtures/ldiff/output.diff.issue95_trailing_context-u".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line1".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line1-c".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line1-e".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line1-f".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line1-u".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line2".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line2-c".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line2-e".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line2-f".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line2-u".freeze, "spec/fixtures/new-chef".freeze, "spec/fixtures/new-chef2".freeze, "spec/fixtures/old-chef".freeze, "spec/fixtures/old-chef2".freeze, "spec/hunk_spec.rb".freeze, "spec/issues_spec.rb".freeze, "spec/lcs_spec.rb".freeze, "spec/ldiff_spec.rb".freeze, "spec/patch_spec.rb".freeze, "spec/sdiff_spec.rb".freeze, "spec/spec_helper.rb".freeze, "spec/traverse_balanced_spec.rb".freeze, "spec/traverse_sequences_spec.rb".freeze] + s.files = ["CHANGELOG.md".freeze, "CODE_OF_CONDUCT.md".freeze, "CONTRIBUTING.md".freeze, "CONTRIBUTORS.md".freeze, "LICENCE.md".freeze, "Manifest.txt".freeze, "README.md".freeze, "Rakefile".freeze, "SECURITY.md".freeze, "bin/ldiff".freeze, "lib/diff-lcs.rb".freeze, "lib/diff/lcs.rb".freeze, "lib/diff/lcs/array.rb".freeze, "lib/diff/lcs/block.rb".freeze, "lib/diff/lcs/callbacks.rb".freeze, "lib/diff/lcs/change.rb".freeze, "lib/diff/lcs/hunk.rb".freeze, "lib/diff/lcs/internals.rb".freeze, "lib/diff/lcs/ldiff.rb".freeze, "lib/diff/lcs/string.rb".freeze, "lib/diff/lcs/version.rb".freeze, "licenses/COPYING.txt".freeze, "licenses/artistic.txt".freeze, "licenses/dco.txt".freeze, "spec/change_spec.rb".freeze, "spec/diff_spec.rb".freeze, "spec/fixtures/123_x".freeze, "spec/fixtures/456_x".freeze, "spec/fixtures/aX".freeze, "spec/fixtures/bXaX".freeze, "spec/fixtures/ds1.csv".freeze, "spec/fixtures/ds2.csv".freeze, "spec/fixtures/empty".freeze, "spec/fixtures/file1.bin".freeze, "spec/fixtures/file2.bin".freeze, "spec/fixtures/four_lines".freeze, "spec/fixtures/four_lines_with_missing_new_line".freeze, "spec/fixtures/ldiff/diff.missing_new_line1-e".freeze, "spec/fixtures/ldiff/diff.missing_new_line1-f".freeze, "spec/fixtures/ldiff/diff.missing_new_line2-e".freeze, "spec/fixtures/ldiff/diff.missing_new_line2-f".freeze, "spec/fixtures/ldiff/error.diff.chef-e".freeze, "spec/fixtures/ldiff/error.diff.chef-f".freeze, "spec/fixtures/ldiff/error.diff.missing_new_line1-e".freeze, "spec/fixtures/ldiff/error.diff.missing_new_line1-f".freeze, "spec/fixtures/ldiff/error.diff.missing_new_line2-e".freeze, "spec/fixtures/ldiff/error.diff.missing_new_line2-f".freeze, "spec/fixtures/ldiff/output.diff".freeze, "spec/fixtures/ldiff/output.diff-c".freeze, "spec/fixtures/ldiff/output.diff-u".freeze, "spec/fixtures/ldiff/output.diff.bin1".freeze, "spec/fixtures/ldiff/output.diff.bin1-c".freeze, "spec/fixtures/ldiff/output.diff.bin1-e".freeze, "spec/fixtures/ldiff/output.diff.bin1-f".freeze, "spec/fixtures/ldiff/output.diff.bin1-u".freeze, "spec/fixtures/ldiff/output.diff.bin2".freeze, "spec/fixtures/ldiff/output.diff.bin2-c".freeze, "spec/fixtures/ldiff/output.diff.bin2-e".freeze, "spec/fixtures/ldiff/output.diff.bin2-f".freeze, "spec/fixtures/ldiff/output.diff.bin2-u".freeze, "spec/fixtures/ldiff/output.diff.chef".freeze, "spec/fixtures/ldiff/output.diff.chef-c".freeze, "spec/fixtures/ldiff/output.diff.chef-u".freeze, "spec/fixtures/ldiff/output.diff.chef2".freeze, "spec/fixtures/ldiff/output.diff.chef2-c".freeze, "spec/fixtures/ldiff/output.diff.chef2-d".freeze, "spec/fixtures/ldiff/output.diff.chef2-u".freeze, "spec/fixtures/ldiff/output.diff.empty.vs.four_lines".freeze, "spec/fixtures/ldiff/output.diff.empty.vs.four_lines-c".freeze, "spec/fixtures/ldiff/output.diff.empty.vs.four_lines-e".freeze, "spec/fixtures/ldiff/output.diff.empty.vs.four_lines-f".freeze, "spec/fixtures/ldiff/output.diff.empty.vs.four_lines-u".freeze, "spec/fixtures/ldiff/output.diff.four_lines.vs.empty".freeze, "spec/fixtures/ldiff/output.diff.four_lines.vs.empty-c".freeze, "spec/fixtures/ldiff/output.diff.four_lines.vs.empty-e".freeze, "spec/fixtures/ldiff/output.diff.four_lines.vs.empty-f".freeze, "spec/fixtures/ldiff/output.diff.four_lines.vs.empty-u".freeze, "spec/fixtures/ldiff/output.diff.issue95_trailing_context".freeze, "spec/fixtures/ldiff/output.diff.issue95_trailing_context-c".freeze, "spec/fixtures/ldiff/output.diff.issue95_trailing_context-e".freeze, "spec/fixtures/ldiff/output.diff.issue95_trailing_context-f".freeze, "spec/fixtures/ldiff/output.diff.issue95_trailing_context-u".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line1".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line1-c".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line1-e".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line1-f".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line1-u".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line2".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line2-c".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line2-e".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line2-f".freeze, "spec/fixtures/ldiff/output.diff.missing_new_line2-u".freeze, "spec/fixtures/new-chef".freeze, "spec/fixtures/new-chef2".freeze, "spec/fixtures/old-chef".freeze, "spec/fixtures/old-chef2".freeze, "spec/hunk_spec.rb".freeze, "spec/issues_spec.rb".freeze, "spec/lcs_spec.rb".freeze, "spec/ldiff_spec.rb".freeze, "spec/patch_spec.rb".freeze, "spec/sdiff_spec.rb".freeze, "spec/spec_helper.rb".freeze, "spec/traverse_balanced_spec.rb".freeze, "spec/traverse_sequences_spec.rb".freeze] s.homepage = "https://github.com/halostatue/diff-lcs".freeze s.licenses = ["MIT".freeze, "Artistic-1.0-Perl".freeze, "GPL-2.0-or-later".freeze] s.rdoc_options = ["--main".freeze, "README.md".freeze] - s.required_ruby_version = Gem::Requirement.new(">= 1.8".freeze) + s.required_ruby_version = Gem::Requirement.new([">= 3.2.0".freeze, "< 5".freeze]) s.rubygems_version = "3.6.9".freeze s.summary = "Diff::LCS computes the difference between two Enumerable sequences using the McIlroy-Hunt longest common subsequence (LCS) algorithm".freeze @@ -26,8 +26,12 @@ Gem::Specification.new do |s| s.add_development_dependency(%q.freeze, ["~> 4.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 2.1".freeze, ">= 2.1.1".freeze]) - s.add_development_dependency(%q.freeze, ["~> 1.6".freeze]) s.add_development_dependency(%q.freeze, [">= 2.0".freeze, "< 4".freeze]) s.add_development_dependency(%q.freeze, [">= 10.0".freeze, "< 14".freeze]) s.add_development_dependency(%q.freeze, [">= 6.3.1".freeze, "< 7".freeze]) + s.add_development_dependency(%q.freeze, ["~> 0.9".freeze]) + s.add_development_dependency(%q.freeze, ["~> 0.9".freeze]) + s.add_development_dependency(%q.freeze, ["~> 1.50".freeze]) + s.add_development_dependency(%q.freeze, ["~> 1.0".freeze]) + s.add_development_dependency(%q.freeze, ["~> 0.11".freeze]) end diff --git a/lib/diff/lcs.rb b/lib/diff/lcs.rb index 5ee89377..fbe9ca31 100644 --- a/lib/diff/lcs.rb +++ b/lib/diff/lcs.rb @@ -2,52 +2,62 @@ module Diff; end unless defined? Diff -# == How Diff Works (by Mark-Jason Dominus) +# ## How Diff Works (by Mark-Jason Dominus) # -# I once read an article written by the authors of +diff+; they said that they -# hard worked very hard on the algorithm until they found the right one. +# I once read an article written by the authors of `diff`; they said that they hard worked +# very hard on the algorithm until they found the right one. # -# I think what they ended up using (and I hope someone will correct me, because -# I am not very confident about this) was the `longest common subsequence' -# method. In the LCS problem, you have two sequences of items: +# I think what they ended up using (and I hope someone will correct me, because I am not +# very confident about this) was the `longest common subsequence' method. In the LCS +# problem, you have two sequences of items: # -# a b c d f g h j q z -# a b c d e f g i j k r x y z +# ``` +# a b c d f g h j q z +# a b c d e f g i j k r x y z +# ``` # -# and you want to find the longest sequence of items that is present in both -# original sequences in the same order. That is, you want to find a new -# sequence *S* which can be obtained from the first sequence by deleting some -# items, and from the second sequence by deleting other items. You also want -# *S* to be as long as possible. In this case *S* is: +# and you want to find the longest sequence of items that is present in both original +# sequences in the same order. That is, you want to find a new sequence *S* which can be +# obtained from the first sequence by deleting some items, and from the second sequence by +# deleting other items. You also want *S* to be as long as possible. In this case *S* is: # -# a b c d f g j z +# ``` +# a b c d f g j z +# ``` # # From there it's only a small step to get diff-like output: # -# e h i k q r x y -# + - + + - + + + +# ``` +# e h i k q r x y +# + - + + - + + + +# ``` # -# This module solves the LCS problem. It also includes a canned function to -# generate +diff+-like output. +# This module solves the LCS problem. It also includes a canned function to generate +# `diff`-like output. # -# It might seem from the example above that the LCS of two sequences is always -# pretty obvious, but that's not always the case, especially when the two -# sequences have many repeated elements. For example, consider +# It might seem from the example above that the LCS of two sequences is always pretty +# obvious, but that's not always the case, especially when the two sequences have many +# repeated elements. For example, consider # -# a x b y c z p d q -# a b c a x b y c z +# ``` +# a x b y c z p d q +# a b c a x b y c z +# ``` # -# A naive approach might start by matching up the +a+ and +b+ that appear at +# A naive approach might start by matching up the `a` and `b` that appear at # the beginning of each sequence, like this: # -# a x b y c z p d q -# a b c a b y c z +# ``` +# a x b y c z p d q +# a b c a b y c z +# ``` # -# This finds the common subsequence +a b c z+. But actually, the LCS is +a x b -# y c z+: +# This finds the common subsequence `a b c z`. But actually, the LCS is `a x b y c z`: # -# a x b y c z p d q -# a b c a x b y c z +# ``` +# a x b y c z p d q +# a b c a x b y c z +# ``` module Diff::LCS end @@ -56,71 +66,47 @@ module Diff::LCS require "diff/lcs/internals" module Diff::LCS - # Returns an Array containing the longest common subsequence(s) between - # +self+ and +other+. See Diff::LCS#lcs. - # - # lcs = seq1.lcs(seq2) - # - # A note when using objects: Diff::LCS only works properly when each object - # can be used as a key in a Hash. This means that those objects must implement - # the methods +#hash+ and +#eql?+ such that two objects containing identical values - # compare identically for key purposes. That is: - # - # O.new('a').eql?(O.new('a')) == true && - # O.new('a').hash == O.new('a').hash - def lcs(other, &block) # :yields: self[i] if there are matched subsequences + # Returns an Array containing the longest common subsequence(s) between `self` and + # `other`. See Diff::LCS.lcs. + def lcs(other, &block) = # :yields: self[i] if there are matched subsequences Diff::LCS.lcs(self, other, &block) - end - # Returns the difference set between +self+ and +other+. See Diff::LCS#diff. - def diff(other, callbacks = nil, &block) - Diff::LCS.diff(self, other, callbacks, &block) - end + # Returns the difference set between `self` and `other`. See Diff::LCS.diff. + def diff(other, callbacks = nil, &block) = Diff::LCS.diff(self, other, callbacks, &block) - # Returns the balanced ("side-by-side") difference set between +self+ and - # +other+. See Diff::LCS#sdiff. - def sdiff(other, callbacks = nil, &block) - Diff::LCS.sdiff(self, other, callbacks, &block) - end + # Returns the balanced ("side-by-side") difference set between `self` and `other`. See + # Diff::LCS.sdiff. + def sdiff(other, callbacks = nil, &block) = Diff::LCS.sdiff(self, other, callbacks, &block) - # Traverses the discovered longest common subsequences between +self+ and - # +other+. See Diff::LCS#traverse_sequences. - def traverse_sequences(other, callbacks = nil, &block) + # Traverses the discovered longest common subsequences between `self` and `other`. See + # Diff::LCS.traverse_sequences. + def traverse_sequences(other, callbacks = nil, &block) = Diff::LCS.traverse_sequences(self, other, callbacks || Diff::LCS::SequenceCallbacks, &block) - end - # Traverses the discovered longest common subsequences between +self+ and - # +other+ using the alternate, balanced algorithm. See - # Diff::LCS#traverse_balanced. - def traverse_balanced(other, callbacks = nil, &block) + # Traverses the discovered longest common subsequences between `self` and `other` using + # the alternate, balanced algorithm. See Diff::LCS.traverse_balanced. + def traverse_balanced(other, callbacks = nil, &block) = Diff::LCS.traverse_balanced(self, other, callbacks || Diff::LCS::BalancedCallbacks, &block) - end - # Attempts to patch +self+ with the provided +patchset+. A new sequence based - # on +self+ and the +patchset+ will be created. See Diff::LCS#patch. Attempts - # to autodiscover the direction of the patch. - def patch(patchset) - Diff::LCS.patch(self, patchset) - end + # Attempts to patch `self` with the provided `patchset`. A new sequence based on `self` + # and the `patchset` will be created. See Diff::LCS.patch. Attempts to autodiscover the + # direction of the patch. + def patch(patchset) = Diff::LCS.patch(self, patchset) alias_method :unpatch, :patch - # Attempts to patch +self+ with the provided +patchset+. A new sequence based - # on +self+ and the +patchset+ will be created. See Diff::LCS#patch. Does no - # patch direction autodiscovery. - def patch!(patchset) - Diff::LCS.patch!(self, patchset) - end + # Attempts to patch `self` with the provided `patchset`. A new sequence based on `self` + # and the `patchset` will be created. See Diff::LCS.patch!. Does no patch direction + # autodiscovery. + def patch!(patchset) = Diff::LCS.patch!(self, patchset) - # Attempts to unpatch +self+ with the provided +patchset+. A new sequence - # based on +self+ and the +patchset+ will be created. See Diff::LCS#unpatch. - # Does no patch direction autodiscovery. - def unpatch!(patchset) - Diff::LCS.unpatch!(self, patchset) - end + # Attempts to unpatch `self` with the provided `patchset`. A new sequence based on + # `self` and the `patchset` will be created. See Diff::LCS.unpatch!. Does no patch + # direction autodiscovery. + def unpatch!(patchset) = Diff::LCS.unpatch!(self, patchset) - # Attempts to patch +self+ with the provided +patchset+, using #patch!. If - # the sequence this is used on supports #replace, the value of +self+ will be - # replaced. See Diff::LCS#patch. Does no patch direction autodiscovery. + # Attempts to patch `self` with the provided `patchset`, using #patch!. If the sequence + # this is used on supports #replace, the value of `self` will be replaced. See + # Diff::LCS.patch!. Does no patch direction autodiscovery. def patch_me(patchset) if respond_to? :replace replace(patch!(patchset)) @@ -129,9 +115,9 @@ def patch_me(patchset) end end - # Attempts to unpatch +self+ with the provided +patchset+, using #unpatch!. - # If the sequence this is used on supports #replace, the value of +self+ will - # be replaced. See Diff::LCS#unpatch. Does no patch direction autodiscovery. + # Attempts to unpatch `self` with the provided `patchset`, using #unpatch!. If the + # sequence this is used on supports #replace, the value of `self` will be replaced. See + # Diff::LCS#unpatch. Does no patch direction autodiscovery. def unpatch_me(patchset) if respond_to? :replace replace(unpatch!(patchset)) @@ -139,440 +125,427 @@ def unpatch_me(patchset) unpatch!(patchset) end end -end -class << Diff::LCS - def lcs(seq1, seq2, &block) # :yields: seq1[i] for each matched + # Returns an Array containing the longest common subsequence(s) between `seq` and + # `seq2`. + # + # > NOTE on comparing objects: Diff::LCS only works properly when each object can be + # > used as a key in a Hash. This means that those objects must implement the methods + # > `#hash` and `#eql?` such that two objects containing identical values compare + # > identically for key purposes. That is: + # > + # > ``` + # > O.new('a').eql?(O.new('a')) == true && O.new('a').hash == O.new('a').hash + # > ``` + def self.lcs(seq1, seq2, &block) # :yields: seq1[i] for each matched matches = Diff::LCS::Internals.lcs(seq1, seq2) - ret = [] - string = seq1.is_a? String - matches.each_index do |i| - next if matches[i].nil? - - v = string ? seq1[i, 1] : seq1[i] - v = block[v] if block - ret << v - end - ret + [].tap { |result| + matches.each_index do + next if matches[_1].nil? + + v = seq1[_1] + v = block.call(v) if block + + result << v + end + } end - alias_method :LCS, :lcs - - # #diff computes the smallest set of additions and deletions necessary to - # turn the first sequence into the second, and returns a description of these - # changes. - # - # See Diff::LCS::DiffCallbacks for the default behaviour. An alternate - # behaviour may be implemented with Diff::LCS::ContextDiffCallbacks. If a - # Class argument is provided for +callbacks+, #diff will attempt to - # initialise it. If the +callbacks+ object (possibly initialised) responds to - # #finish, it will be called. - def diff(seq1, seq2, callbacks = nil, &block) # :yields: diff changes + + # `diff` computes the smallest set of additions and deletions necessary to turn the + # first sequence into the second, and returns a description of these changes. + # + # See Diff::LCS::DiffCallbacks for the default behaviour. An alternate behaviour may be + # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds + # to #finish, it will be called. + def self.diff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:diff, seq1, seq2, callbacks || Diff::LCS::DiffCallbacks, &block) - end - # #sdiff computes all necessary components to show two sequences and their - # minimized differences side by side, just like the Unix utility - # sdiff does: - # - # old < - - # same same - # before | after - # - > new - # - # See Diff::LCS::SDiffCallbacks for the default behaviour. An alternate - # behaviour may be implemented with Diff::LCS::ContextDiffCallbacks. If a - # Class argument is provided for +callbacks+, #diff will attempt to - # initialise it. If the +callbacks+ object (possibly initialised) responds to - # #finish, it will be called. - # - # Each element of a returned array is a Diff::LCS::ContextChange object, - # which can be implicitly converted to an array. - # - # Diff::LCS.sdiff(a, b).each do |action, (old_pos, old_element), (new_pos, new_element)| - # case action - # when '!' - # # replace - # when '-' - # # delete - # when '+' - # # insert - # end + # `sdiff` computes all necessary components to show two sequences and their minimized + # differences side by side, just like the Unix utility _sdiff_ does: + # + # + # ``` + # old < - + # same same + # before | after + # - > new + # ``` + # + # See Diff::LCS::SDiffCallbacks for the default behaviour. An alternate behaviour may be + # implemented with Diff::LCS::ContextDiffCallbacks. If the `callbacks` object responds + # to #finish, it will be called. + # + # Each element of a returned array is a Diff::LCS::ContextChange object, which can be + # implicitly converted to an array. + # + # ```ruby + # Diff::LCS.sdiff(a, b).each do |action, (old_pos, old_element), (new_pos, new_element)| + # case action + # when '!' + # # replace + # when '-' + # # delete + # when '+' + # # insert # end - def sdiff(seq1, seq2, callbacks = nil, &block) # :yields: diff changes + # end + # ``` + def self.sdiff(seq1, seq2, callbacks = nil, &block) = # :yields: diff changes diff_traversal(:sdiff, seq1, seq2, callbacks || Diff::LCS::SDiffCallbacks, &block) - end - # #traverse_sequences is the most general facility provided by this module; - # #diff and #lcs are implemented as calls to it. - # - # The arguments to #traverse_sequences are the two sequences to traverse, and - # a callback object, like this: - # - # traverse_sequences(seq1, seq2, Diff::LCS::ContextDiffCallbacks.new) - # - # == Callback Methods - # - # Optional callback methods are emphasized. - # - # callbacks#match:: Called when +a+ and +b+ are pointing to - # common elements in +A+ and +B+. - # callbacks#discard_a:: Called when +a+ is pointing to an - # element not in +B+. - # callbacks#discard_b:: Called when +b+ is pointing to an - # element not in +A+. - # callbacks#finished_a:: Called when +a+ has reached the end of - # sequence +A+. - # callbacks#finished_b:: Called when +b+ has reached the end of - # sequence +B+. - # - # == Algorithm - # - # a---+ - # v - # A = a b c e h j l m n p - # B = b c d e f j k l m r s t - # ^ - # b---+ - # - # If there are two arrows (+a+ and +b+) pointing to elements of sequences +A+ - # and +B+, the arrows will initially point to the first elements of their - # respective sequences. #traverse_sequences will advance the arrows through - # the sequences one element at a time, calling a method on the user-specified - # callback object before each advance. It will advance the arrows in such a - # way that if there are elements A[i] and B[j] which are - # both equal and part of the longest common subsequence, there will be some - # moment during the execution of #traverse_sequences when arrow +a+ is - # pointing to A[i] and arrow +b+ is pointing to B[j]. When - # this happens, #traverse_sequences will call callbacks#match and - # then it will advance both arrows. - # - # Otherwise, one of the arrows is pointing to an element of its sequence that - # is not part of the longest common subsequence. #traverse_sequences will - # advance that arrow and will call callbacks#discard_a or - # callbacks#discard_b, depending on which arrow it advanced. If both - # arrows point to elements that are not part of the longest common - # subsequence, then #traverse_sequences will advance arrow +a+ and call the - # appropriate callback, then it will advance arrow +b+ and call the appropriate - # callback. - # - # The methods for callbacks#match, callbacks#discard_a, and - # callbacks#discard_b are invoked with an event comprising the - # action ("=", "+", or "-", respectively), the indexes +i+ and +j+, and the - # elements A[i] and B[j]. Return values are discarded by - # #traverse_sequences. - # - # === End of Sequences - # - # If arrow +a+ reaches the end of its sequence before arrow +b+ does, - # #traverse_sequence will try to call callbacks#finished_a with the - # last index and element of +A+ (A[-1]) and the current index and - # element of +B+ (B[j]). If callbacks#finished_a does not - # exist, then callbacks#discard_b will be called on each element of - # +B+ until the end of the sequence is reached (the call will be done with - # A[-1] and B[j] for each element). - # - # If +b+ reaches the end of +B+ before +a+ reaches the end of +A+, - # callbacks#finished_b will be called with the current index and - # element of +A+ (A[i]) and the last index and element of +B+ - # (A[-1]). Again, if callbacks#finished_b does not exist on - # the callback object, then callbacks#discard_a will be called on - # each element of +A+ until the end of the sequence is reached (A[i] - # and B[-1]). - # - # There is a chance that one additional callbacks#discard_a or - # callbacks#discard_b will be called after the end of the sequence - # is reached, if +a+ has not yet reached the end of +A+ or +b+ has not yet - # reached the end of +B+. - def traverse_sequences(seq1, seq2, callbacks = Diff::LCS::SequenceCallbacks) # :yields: change events + # #traverse_sequences is the most general facility provided by this module; #diff and + # #lcs are implemented using #traverse_sequences. + # + # The arguments to #traverse_sequence are the two sequences to traverse, and a callback + # object, like this: + # + # ```ruby + # traverse_sequences(seq1, seq2, Diff::LCS::ContextDiffCallbacks) + # ``` + # + # ### Callback Methods + # + # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` + # and `B`. + # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. + # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. + # - `callbacks#finished_a`: Called when `a` has reached the end of sequence `A`. + # Optional. + # - `callbacks#finished_b`: Called when `b` has reached the end of sequence `B`. + # Optional. + # + # ### Algorithm + # + # ``` + # a---+ + # v + # A = a b c e h j l m n p + # B = b c d e f j k l m r s t + # ^ + # b---+ + # ``` + # + # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, + # the arrows will initially point to the first elements of their respective sequences. + # #traverse_sequences will advance the arrows through the sequences one element at + # a time, calling a method on the user-specified callback object before each advance. It + # will advance the arrows in such a way that if there are elements `A[i]` and `B[j]` + # which are both equal and part of the longest common subsequence, there will be some + # moment during the execution of #traverse_sequences when arrow `a` is pointing to + # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences + # will call `callbacks#match` and then it will advance both arrows. + # + # Otherwise, one of the arrows is pointing to an element of its sequence that is not + # part of the longest common subsequence. #traverse_sequences will advance that arrow + # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow + # it advanced. If both arrows point to elements that are not part of the longest common + # subsequence, then #traverse_sequences will advance arrow `a` and call the appropriate + # callback, then it will advance arrow `b` and call the appropriate callback. + # + # The methods for `callbacks#match`, `callbacks#discard_a`, and `callbacks#discard_b` + # are invoked with an event comprising the action ("=", "+", or "-", respectively), the + # indexes `i` and `j`, and the elements `A[i]` and `B[j]`. Return values are discarded + # by #traverse_sequences. + # + # #### End of Sequences + # + # If arrow `a` reaches the end of its sequence before arrow `b` does, #traverse_sequence + # will try to call `callbacks#finished_a` with the last index and element of `A` + # (`A[-1]`) and the current index and element of `B` (`B[j]`). If `callbacks#finished_a` + # does not exist, then `callbacks#discard_b` will be called on each element of `B` until + # the end of the sequence is reached (the call will be done with `A[-1]` and `B[j]` for + # each element). + # + # If `b` reaches the end of `B` before `a` reaches the end of `A`, + # `callbacks#finished_b` will be called with the current index and element of `A` + # (`A[i]`) and the last index and element of `B` (`A[-1]`). Again, if + # `callbacks#finished_b` does not exist on the callback object, then + # `callbacks#discard_a` will be called on each element of `A` until the end of the + # sequence is reached (`A[i]` and `B[-1]`). + # + # There is a chance that one additional `callbacks#discard_a` or `callbacks#discard_b` + # will be called after the end of the sequence is reached, if `a` has not yet reached + # the end of `A` or `b` has not yet reached the end of `B`. + def self.traverse_sequences(seq1, seq2, callbacks = nil) # :yields: change events callbacks ||= Diff::LCS::SequenceCallbacks matches = Diff::LCS::Internals.lcs(seq1, seq2) run_finished_a = run_finished_b = false - string = seq1.is_a?(String) a_size = seq1.size b_size = seq2.size - ai = bj = 0 + a_i = b_j = 0 matches.each do |b_line| if b_line.nil? - unless seq1[ai].nil? - ax = string ? seq1[ai, 1] : seq1[ai] - bx = string ? seq2[bj, 1] : seq2[bj] + unless seq1[a_i].nil? + a_x = seq1[a_i] + b_x = seq2[b_j] - event = Diff::LCS::ContextChange.new("-", ai, ax, bj, bx) + event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) end else - ax = string ? seq1[ai, 1] : seq1[ai] + a_x = seq1[a_i] loop do - break unless bj < b_line + break unless b_j < b_line - bx = string ? seq2[bj, 1] : seq2[bj] - event = Diff::LCS::ContextChange.new("+", ai, ax, bj, bx) + b_x = seq2[b_j] + event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) - bj += 1 + b_j += 1 end - bx = string ? seq2[bj, 1] : seq2[bj] - event = Diff::LCS::ContextChange.new("=", ai, ax, bj, bx) + b_x = seq2[b_j] + event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) - bj += 1 + b_j += 1 end - ai += 1 + + a_i += 1 end - # The last entry (if any) processed was a match. +ai+ and +bj+ point just - # past the last matching lines in their sequences. - while (ai < a_size) || (bj < b_size) + # The last entry (if any) processed was a match. `a_i` and `b_j` point just past the + # last matching lines in their sequences. + while (a_i < a_size) || (b_j < b_size) # last A? - if ai == a_size && bj < b_size + if a_i == a_size && b_j < b_size if callbacks.respond_to?(:finished_a) && !run_finished_a - ax = string ? seq1[-1, 1] : seq1[-1] - bx = string ? seq2[bj, 1] : seq2[bj] - event = Diff::LCS::ContextChange.new(">", a_size - 1, ax, bj, bx) + a_x = seq1[-1] + b_x = seq2[b_j] + event = Diff::LCS::ContextChange.new(">", a_size - 1, a_x, b_j, b_x) event = yield event if block_given? callbacks.finished_a(event) run_finished_a = true else - ax = string ? seq1[ai, 1] : seq1[ai] + a_x = seq1[a_i] loop do - bx = string ? seq2[bj, 1] : seq2[bj] - event = Diff::LCS::ContextChange.new("+", ai, ax, bj, bx) + b_x = seq2[b_j] + event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) - bj += 1 - break unless bj < b_size + b_j += 1 + break unless b_j < b_size end end end # last B? - if bj == b_size && ai < a_size + if b_j == b_size && a_i < a_size if callbacks.respond_to?(:finished_b) && !run_finished_b - ax = string ? seq1[ai, 1] : seq1[ai] - bx = string ? seq2[-1, 1] : seq2[-1] - event = Diff::LCS::ContextChange.new("<", ai, ax, b_size - 1, bx) + a_x = seq1[a_i] + b_x = seq2[-1] + event = Diff::LCS::ContextChange.new("<", a_i, a_x, b_size - 1, b_x) event = yield event if block_given? callbacks.finished_b(event) run_finished_b = true else - bx = string ? seq2[bj, 1] : seq2[bj] + b_x = seq2[b_j] loop do - ax = string ? seq1[ai, 1] : seq1[ai] - event = Diff::LCS::ContextChange.new("-", ai, ax, bj, bx) + a_x = seq1[a_i] + event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) - ai += 1 - break unless bj < b_size + a_i += 1 + break unless b_j < b_size end end end - if ai < a_size - ax = string ? seq1[ai, 1] : seq1[ai] - bx = string ? seq2[bj, 1] : seq2[bj] - event = Diff::LCS::ContextChange.new("-", ai, ax, bj, bx) + if a_i < a_size + a_x = seq1[a_i] + b_x = seq2[b_j] + event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) - ai += 1 + a_i += 1 end - if bj < b_size - ax = string ? seq1[ai, 1] : seq1[ai] - bx = string ? seq2[bj, 1] : seq2[bj] - event = Diff::LCS::ContextChange.new("+", ai, ax, bj, bx) + if b_j < b_size + a_x = seq1[a_i] + b_x = seq2[b_j] + event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) - bj += 1 + b_j += 1 end end end - # #traverse_balanced is an alternative to #traverse_sequences. It uses a - # different algorithm to iterate through the entries in the computed longest - # common subsequence. Instead of viewing the changes as insertions or - # deletions from one of the sequences, #traverse_balanced will report - # changes between the sequences. - # - # The arguments to #traverse_balanced are the two sequences to traverse and a - # callback object, like this: - # - # traverse_balanced(seq1, seq2, Diff::LCS::ContextDiffCallbacks.new) - # - # #sdiff is implemented with #traverse_balanced. - # - # == Callback Methods - # - # Optional callback methods are emphasized. - # - # callbacks#match:: Called when +a+ and +b+ are pointing to - # common elements in +A+ and +B+. - # callbacks#discard_a:: Called when +a+ is pointing to an - # element not in +B+. - # callbacks#discard_b:: Called when +b+ is pointing to an - # element not in +A+. - # callbacks#change:: Called when +a+ and +b+ are pointing to - # the same relative position, but - # A[a] and B[b] are not - # the same; a change has - # occurred. - # - # #traverse_balanced might be a bit slower than #traverse_sequences, - # noticeable only while processing huge amounts of data. - # - # == Algorithm - # - # a---+ - # v - # A = a b c e h j l m n p - # B = b c d e f j k l m r s t - # ^ - # b---+ - # - # === Matches - # - # If there are two arrows (+a+ and +b+) pointing to elements of sequences +A+ - # and +B+, the arrows will initially point to the first elements of their - # respective sequences. #traverse_sequences will advance the arrows through - # the sequences one element at a time, calling a method on the user-specified - # callback object before each advance. It will advance the arrows in such a - # way that if there are elements A[i] and B[j] which are - # both equal and part of the longest common subsequence, there will be some - # moment during the execution of #traverse_sequences when arrow +a+ is - # pointing to A[i] and arrow +b+ is pointing to B[j]. When - # this happens, #traverse_sequences will call callbacks#match and - # then it will advance both arrows. - # - # === Discards - # - # Otherwise, one of the arrows is pointing to an element of its sequence that - # is not part of the longest common subsequence. #traverse_sequences will - # advance that arrow and will call callbacks#discard_a or - # callbacks#discard_b, depending on which arrow it advanced. - # - # === Changes - # - # If both +a+ and +b+ point to elements that are not part of the longest - # common subsequence, then #traverse_sequences will try to call - # callbacks#change and advance both arrows. If - # callbacks#change is not implemented, then - # callbacks#discard_a and callbacks#discard_b will be - # called in turn. - # - # The methods for callbacks#match, callbacks#discard_a, - # callbacks#discard_b, and callbacks#change are invoked - # with an event comprising the action ("=", "+", "-", or "!", respectively), - # the indexes +i+ and +j+, and the elements A[i] and B[j]. + # #traverse_balanced is an alternative to #traverse_sequences. It uses a different + # algorithm to iterate through the entries in the computed longest common subsequence. + # Instead of viewing the changes as insertions or deletions from one of the sequences, + # #traverse_balanced will report _changes_ between the sequences. + # + # The arguments to #traverse_balanced are the two sequences to traverse and a callback + # object, like this: + # + # ```ruby + # traverse_balanced(seq1, seq2, Diff::LCS::ContextDiffCallbacks) + # ``` + # + # #sdiff is implemented using #traverse_balanced. + # + # ### Callback Methods + # + # - `callbacks#match`: Called when `a` and `b` are pointing to common elements in `A` + # and `B`. + # - `callbacks#discard_a`: Called when `a` is pointing to an element not in `B`. + # - `callbacks#discard_b`: Called when `b` is pointing to an element not in `A`. + # - `callbacks#change`: Called when `a` and `b` are pointing to the same relative + # position, but `A[a]` and `B[b]` are not the same; a _change_ has occurred. Optional. + # + # #traverse_balanced might be a bit slower than #traverse_sequences, noticeable only + # while processing large amounts of data. + # + # ### Algorithm + # + # ``` + # a---+ + # v + # A = a b c e h j l m n p + # B = b c d e f j k l m r s t + # ^ + # b---+ + # ``` + # + # #### Matches + # + # If there are two arrows (`a` and `b`) pointing to elements of sequences `A` and `B`, + # the arrows will initially point to the first elements of their respective sequences. + # #traverse_sequences will advance the arrows through the sequences one element at + # a time, calling a method on the user-specified callback object before each advance. It + # will advance the arrows in such a way that if there are elements `A[i]` and + # `B[j]` which are both equal and part of the longest common subsequence, there will be + # some moment during the execution of #traverse_sequences when arrow `a` is pointing to + # `A[i]` and arrow `b` is pointing to `B[j]`. When this happens, #traverse_sequences + # will call `callbacks#match` and then it will advance both arrows. + # + # #### Discards + # + # Otherwise, one of the arrows is pointing to an element of its sequence that is not + # part of the longest common subsequence. #traverse_sequences will advance that arrow + # and will call `callbacks#discard_a` or `callbacks#discard_b`, depending on which arrow + # it advanced. + # + # #### Changes + # + # If both `a` and `b` point to elements that are not part of the longest common + # subsequence, then #traverse_sequences will try to call `callbacks#change` and advance + # both arrows. If `callbacks#change` is not implemented, then `callbacks#discard_a` and + # `callbacks#discard_b` will be called in turn. + # + # The methods for `callbacks#match`, `callbacks#discard_a`, `callbacks#discard_b`, and + # `callbacks#change` are invoked with an event comprising the action ("=", "+", "-", or + # "!", respectively), the indexes `i` and `j`, and the elements `A[i]` and `B[j]`. # Return values are discarded by #traverse_balanced. # # === Context # - # Note that +i+ and +j+ may not be the same index position, even if +a+ and - # +b+ are considered to be pointing to matching or changed elements. - def traverse_balanced(seq1, seq2, callbacks = Diff::LCS::BalancedCallbacks) + # Note that `i` and `j` may not be the same index position, even if `a` and `b` are + # considered to be pointing to matching or changed elements. + def self.traverse_balanced(seq1, seq2, callbacks = Diff::LCS::BalancedCallbacks) matches = Diff::LCS::Internals.lcs(seq1, seq2) a_size = seq1.size b_size = seq2.size - ai = bj = mb = 0 - ma = -1 - string = seq1.is_a?(String) + a_i = b_j = m_b = 0 + m_a = -1 # Process all the lines in the match vector. loop do - # Find next match indexes +ma+ and +mb+ + # Find next match indexes `m_a` and `m_b` loop do - ma += 1 - break unless ma < matches.size && matches[ma].nil? + m_a += 1 + break unless m_a < matches.size && matches[m_a].nil? end - break if ma >= matches.size # end of matches? + break if m_a >= matches.size # end of matches? - mb = matches[ma] + m_b = matches[m_a] # Change(seq2) - while (ai < ma) || (bj < mb) - ax = string ? seq1[ai, 1] : seq1[ai] - bx = string ? seq2[bj, 1] : seq2[bj] + while (a_i < m_a) || (b_j < m_b) + a_x = seq1[a_i] + b_x = seq2[b_j] - case [(ai < ma), (bj < mb)] + case [(a_i < m_a), (b_j < m_b)] when [true, true] if callbacks.respond_to?(:change) - event = Diff::LCS::ContextChange.new("!", ai, ax, bj, bx) + event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) - ai += 1 + a_i += 1 else - event = Diff::LCS::ContextChange.new("-", ai, ax, bj, bx) + event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) - ai += 1 - ax = string ? seq1[ai, 1] : seq1[ai] - event = Diff::LCS::ContextChange.new("+", ai, ax, bj, bx) + a_i += 1 + a_x = seq1[a_i] + event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end - bj += 1 + b_j += 1 when [true, false] - event = Diff::LCS::ContextChange.new("-", ai, ax, bj, bx) + event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) - ai += 1 + a_i += 1 when [false, true] - event = Diff::LCS::ContextChange.new("+", ai, ax, bj, bx) + event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) - bj += 1 + b_j += 1 end end # Match - ax = string ? seq1[ai, 1] : seq1[ai] - bx = string ? seq2[bj, 1] : seq2[bj] - event = Diff::LCS::ContextChange.new("=", ai, ax, bj, bx) + a_x = seq1[a_i] + b_x = seq2[b_j] + event = Diff::LCS::ContextChange.new("=", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.match(event) - ai += 1 - bj += 1 + a_i += 1 + b_j += 1 end - while (ai < a_size) || (bj < b_size) - ax = string ? seq1[ai, 1] : seq1[ai] - bx = string ? seq2[bj, 1] : seq2[bj] + while (a_i < a_size) || (b_j < b_size) + a_x = seq1[a_i] + b_x = seq2[b_j] - case [(ai < a_size), (bj < b_size)] + case [(a_i < a_size), (b_j < b_size)] when [true, true] if callbacks.respond_to?(:change) - event = Diff::LCS::ContextChange.new("!", ai, ax, bj, bx) + event = Diff::LCS::ContextChange.new("!", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.change(event) - ai += 1 + a_i += 1 else - event = Diff::LCS::ContextChange.new("-", ai, ax, bj, bx) + event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) - ai += 1 - ax = string ? seq1[ai, 1] : seq1[ai] - event = Diff::LCS::ContextChange.new("+", ai, ax, bj, bx) + a_i += 1 + a_x = seq1[a_i] + event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) end - bj += 1 + b_j += 1 when [true, false] - event = Diff::LCS::ContextChange.new("-", ai, ax, bj, bx) + event = Diff::LCS::ContextChange.new("-", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_a(event) - ai += 1 + a_i += 1 when [false, true] - event = Diff::LCS::ContextChange.new("+", ai, ax, bj, bx) + event = Diff::LCS::ContextChange.new("+", a_i, a_x, b_j, b_x) event = yield event if block_given? callbacks.discard_b(event) - bj += 1 + b_j += 1 end end end @@ -582,61 +555,67 @@ def traverse_balanced(seq1, seq2, callbacks = Diff::LCS::BalancedCallbacks) :patch => {"+" => "+", "-" => "-", "!" => "!", "=" => "="}.freeze, :unpatch => {"+" => "-", "-" => "+", "!" => "!", "=" => "="}.freeze }.freeze + private_constant :PATCH_MAP # standard:enable Style/HashSyntax - # Applies a +patchset+ to the sequence +src+ according to the +direction+ - # (:patch or :unpatch), producing a new sequence. + # Applies a `patchset` to the sequence `src` according to the `direction` (`:patch` or + # `:unpatch`), producing a new sequence. # - # If the +direction+ is not specified, Diff::LCS::patch will attempt to - # discover the direction of the +patchset+. + # If the `direction` is not specified, Diff::LCS::patch will attempt to discover the + # direction of the `patchset`. # - # A +patchset+ can be considered to apply forward (:patch) if the - # following expression is true: + # A `patchset` can be considered to apply forward (`:patch`) if the following expression + # is true: # - # patch(s1, diff(s1, s2)) -> s2 + # ```ruby + # patch(s1, diff(s1, s2)) # => s2 + # ``` # - # A +patchset+ can be considered to apply backward (:unpatch) if the - # following expression is true: + # A `patchset` can be considered to apply backward (`:unpatch`) if the following + # expression is true: # - # patch(s2, diff(s1, s2)) -> s1 + # ```ruby + # patch(s2, diff(s1, s2)) # => s1 + # ``` # - # If the +patchset+ contains no changes, the +src+ value will be returned as - # either src.dup or +src+. A +patchset+ can be deemed as having no - # changes if the following predicate returns true: + # If the `patchset` contains no changes, the `src` value will be returned as either + # `src.dup` or `src`. A `patchset` can be deemed as having no changes if the following + # predicate returns true: # - # patchset.empty? or - # patchset.flatten(1).all? { |change| change.unchanged? } + # ```ruby + # patchset.empty? or patchset.flatten(1).all? { |change| change.unchanged? } + # ``` # - # === Patchsets + # ### Patchsets # - # A +patchset+ is always an enumerable sequence of changes, hunks of changes, - # or a mix of the two. A hunk of changes is an enumerable sequence of - # changes: + # A `patchset` is always an enumerable sequence of changes, hunks of changes, or a mix + # of the two. A hunk of changes is an enumerable sequence of changes: # - # [ # patchset - # # change - # [ # hunk - # # change - # ] - # ] + # ``` + # [ # patchset + # # change + # [ # hunk + # # change + # ] + # ] + # ``` # - # The +patch+ method accepts patchsets that are enumerable sequences - # containing either Diff::LCS::Change objects (or a subclass) or the array - # representations of those objects. Prior to application, array - # representations of Diff::LCS::Change objects will be reified. - def patch(src, patchset, direction = nil) + # The `patch` method accepts `patchset`s that are enumerable sequences containing either + # Diff::LCS::Change objects (or a subclass) or the array representations of those + # objects. Prior to application, array representations of Diff::LCS::Change objects will + # be reified. + def self.patch(src, patchset, direction = nil) # Normalize the patchset. has_changes, patchset = Diff::LCS::Internals.analyze_patchset(patchset) return src.respond_to?(:dup) ? src.dup : src unless has_changes - string = src.is_a?(String) # Start with a new empty type of the source's class res = src.class.new direction ||= Diff::LCS::Internals.intuit_diff_direction(src, patchset) - ai = bj = 0 + a_i = b_j = 0 patch_map = PATCH_MAP[direction] @@ -659,84 +638,78 @@ def patch(src, patchset, direction = nil) case action when "-" # Remove details from the old string - while ai < op - res << (string ? src[ai, 1] : src[ai]) - ai += 1 - bj += 1 + while a_i < op + res << src[a_i] + a_i += 1 + b_j += 1 end - ai += 1 + a_i += 1 when "+" - while bj < np - res << (string ? src[ai, 1] : src[ai]) - ai += 1 - bj += 1 + while b_j < np + res << src[a_i] + a_i += 1 + b_j += 1 end res << el - bj += 1 + b_j += 1 when "=" # This only appears in sdiff output with the SDiff callback. # Therefore, we only need to worry about dealing with a single # element. res << el - ai += 1 - bj += 1 + a_i += 1 + b_j += 1 when "!" - while ai < op - res << (string ? src[ai, 1] : src[ai]) - ai += 1 - bj += 1 + while a_i < op + res << src[a_i] + a_i += 1 + b_j += 1 end - bj += 1 - ai += 1 + b_j += 1 + a_i += 1 res << el end when Diff::LCS::Change case action when "-" - while ai < change.position - res << (string ? src[ai, 1] : src[ai]) - ai += 1 - bj += 1 + while a_i < change.position + res << src[a_i] + a_i += 1 + b_j += 1 end - ai += 1 + a_i += 1 when "+" - while bj < change.position - res << (string ? src[ai, 1] : src[ai]) - ai += 1 - bj += 1 + while b_j < change.position + res << src[a_i] + a_i += 1 + b_j += 1 end - bj += 1 + b_j += 1 res << change.element end end end - while ai < src.size - res << (string ? src[ai, 1] : src[ai]) - ai += 1 - bj += 1 + while a_i < src.size + res << src[a_i] + a_i += 1 + b_j += 1 end res end - # Given a set of patchset, convert the current version to the prior version. - # Does no auto-discovery. - def unpatch!(src, patchset) - patch(src, patchset, :unpatch) - end + # Given a patchset, convert the current version to the prior version. Does no + # auto-discovery. + def self.unpatch!(src, patchset) = patch(src, patchset, :unpatch) - # Given a set of patchset, convert the current version to the next version. - # Does no auto-discovery. - def patch!(src, patchset) - patch(src, patchset, :patch) - end + # Given a patchset, convert the current version to the next version. Does no + # auto-discovery. + def self.patch!(src, patchset) = patch(src, patchset, :patch) end - -require "diff/lcs/backports" diff --git a/lib/diff/lcs/backports.rb b/lib/diff/lcs/backports.rb deleted file mode 100644 index 6543c8a4..00000000 --- a/lib/diff/lcs/backports.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -unless 0.respond_to?(:positive?) - class Fixnum # standard:disable Lint/UnifiedInteger - def positive? - self > 0 - end - - def negative? - self < 0 - end - end -end diff --git a/lib/diff/lcs/block.rb b/lib/diff/lcs/block.rb index 226ed6fa..a44c770c 100644 --- a/lib/diff/lcs/block.rb +++ b/lib/diff/lcs/block.rb @@ -1,37 +1,42 @@ # frozen_string_literal: true -# A block is an operation removing, adding, or changing a group of items. -# Basically, this is just a list of changes, where each change adds or -# deletes a single item. Used by bin/ldiff. -class Diff::LCS::Block - attr_reader :changes, :insert, :remove +Diff::LCS::Block = Data.define(:changes, :insert, :remove) # :nodoc: - def initialize(chunk) - @changes = [] - @insert = [] - @remove = [] +# A block is an operation removing, adding, or changing a group of items, a list of +# changes, where each change adds or deletes a single item. +# +# Used by bin/ldiff. +class Diff::LCS::Block + def self.from_chunk(chunk) + changes, insert, remove = [], [], [] - chunk.each do |item| - @changes << item - @remove << item if item.deleting? - @insert << item if item.adding? + chunk.each do + changes << _1 + remove << _1 if _1.deleting? + insert << _1 if _1.adding? end + + new(changes: changes.freeze, remove: remove.freeze, insert: insert.freeze) end - def diff_size - @insert.size - @remove.size + class << self + private :new, :[] end + private :with + + def diff_size = insert.size - remove.size + def op - case [@remove.empty?, @insert.empty?] - when [false, false] - "!" - when [false, true] - "-" - when [true, false] - "+" - else # [true, true] - "^" + case [remove, insert] + # Unchanged + in [[], []] then "^" + # Delete + in [_, []] then "-" + # Insert + in [[], _] then "+" + # Conflict + in [_, _] then "!" end end end diff --git a/lib/diff/lcs/callbacks.rb b/lib/diff/lcs/callbacks.rb index 2c5a7791..33fc6d0d 100644 --- a/lib/diff/lcs/callbacks.rb +++ b/lib/diff/lcs/callbacks.rb @@ -2,109 +2,98 @@ require "diff/lcs/change" -module Diff::LCS - # This callback object implements the default set of callback events, - # which only returns the event itself. Note that #finished_a and - # #finished_b are not implemented -- I haven't yet figured out where they - # would be useful. - # - # Note that this is intended to be called as is, e.g., - # - # Diff::LCS.LCS(seq1, seq2, Diff::LCS::DefaultCallbacks) - class DefaultCallbacks - class << self - # Called when two items match. - def match(event) - event - end +# This callback object implements the default set of callback events, which only returns +# the event itself. +# +# ```ruby +# Diff::LCS.lcs(seq1, seq2, Diff::LCS::DefaultCallbacks) +# ``` +class Diff::LCS::DefaultCallbacks + # Called when two items match. + def self.match(event) = event - # Called when the old value is discarded in favour of the new value. - def discard_a(event) - event - end + # Called when the old value is discarded in favour of the new value. + def self.discard_a(event) = event - # Called when the new value is discarded in favour of the old value. - def discard_b(event) - event - end + # Called when the new value is discarded in favour of the old value. + def self.discard_b(event) = event - # Called when both the old and new values have changed. - def change(event) - event - end + # Called when both the old and new values have changed. + def self.change(event) = event - private :new - end - end - - # An alias for DefaultCallbacks that is used in - # Diff::LCS#traverse_sequences. - # - # Diff::LCS.LCS(seq1, seq2, Diff::LCS::SequenceCallbacks) - SequenceCallbacks = DefaultCallbacks - - # An alias for DefaultCallbacks that is used in - # Diff::LCS#traverse_balanced. - # - # Diff::LCS.LCS(seq1, seq2, Diff::LCS::BalancedCallbacks) - BalancedCallbacks = DefaultCallbacks + def self.new = self - def self.callbacks_for(callbacks) - callbacks.new - rescue - callbacks + class << self + private :new end end -# This will produce a compound array of simple diff change objects. Each -# element in the #diffs array is a +hunk+ or +hunk+ array, where each -# element in each +hunk+ array is a single Change object representing the -# addition or removal of a single element from one of the two tested -# sequences. The +hunk+ provides the full context for the changes. -# -# diffs = Diff::LCS.diff(seq1, seq2) -# # This example shows a simplified array format. -# # [ [ [ '-', 0, 'a' ] ], # 1 -# # [ [ '+', 2, 'd' ] ], # 2 -# # [ [ '-', 4, 'h' ], # 3 -# # [ '+', 4, 'f' ] ], -# # [ [ '+', 6, 'k' ] ], # 4 -# # [ [ '-', 8, 'n' ], # 5 -# # [ '-', 9, 'p' ], -# # [ '+', 9, 'r' ], -# # [ '+', 10, 's' ], -# # [ '+', 11, 't' ] ] ] -# -# There are five hunks here. The first hunk says that the +a+ at position 0 -# of the first sequence should be deleted ('-'). The second hunk -# says that the +d+ at position 2 of the second sequence should be inserted -# ('+'). The third hunk says that the +h+ at position 4 of the -# first sequence should be removed and replaced with the +f+ from position 4 -# of the second sequence. The other two hunks are described similarly. -# -# === Use -# -# This callback object must be initialised and is used by the Diff::LCS#diff -# method. -# -# cbo = Diff::LCS::DiffCallbacks.new -# Diff::LCS.LCS(seq1, seq2, cbo) -# cbo.finish -# -# Note that the call to #finish is absolutely necessary, or the last set of -# changes will not be visible. Alternatively, can be used as: -# -# cbo = Diff::LCS::DiffCallbacks.new { |tcbo| Diff::LCS.LCS(seq1, seq2, tcbo) } -# -# The necessary #finish call will be made. -# -# === Simplified Array Format -# -# The simplified array format used in the example above can be obtained -# with: -# -# require 'pp' -# pp diffs.map { |e| e.map { |f| f.to_a } } +# An alias for DefaultCallbacks used in Diff::LCS.traverse_sequences. +# +# ```ruby +# Diff::LCS.lcs(seq1, seq2, Diff::LCS::SequenceCallbacks) +# ``` +Diff::LCS::SequenceCallbacks = Diff::LCS::DefaultCallbacks + +# An alias for DefaultCallbacks used in Diff::LCS.traverse_balanced. +# +# ```ruby +# Diff::LCS.lcs(seq1, seq2, Diff::LCS::BalancedCallbacks) +# ``` +Diff::LCS::BalancedCallbacks = Diff::LCS::DefaultCallbacks + +# This will produce a compound array of simple diff change objects. Each element in the +# #diffs array is a `hunk` or `hunk` array, where each element in each `hunk` array is +# a single Change object representing the addition or removal of a single element from one +# of the two tested sequences. The `hunk` provides the full context for the changes. +# +# ```ruby +# diffs = Diff::LCS.diff(seq1, seq2) +# # This example shows a simplified array format. +# # [ [ [ '-', 0, 'a' ] ], # 1 +# # [ [ '+', 2, 'd' ] ], # 2 +# # [ [ '-', 4, 'h' ], # 3 +# # [ '+', 4, 'f' ] ], +# # [ [ '+', 6, 'k' ] ], # 4 +# # [ [ '-', 8, 'n' ], # 5 +# # [ '-', 9, 'p' ], +# # [ '+', 9, 'r' ], +# # [ '+', 10, 's' ], +# # [ '+', 11, 't' ] ] ] +# ``` +# +# There are five hunks here. The first hunk says that the `a` at position 0 of the first +# sequence should be deleted (`'-'`). The second hunk says that the `d` at position 2 of +# the second sequence should be inserted (`'+'`). The third hunk says that the `h` at +# position 4 of the first sequence should be removed and replaced with the `f` from +# position 4 of the second sequence. The other two hunks are described similarly. +# +# ### Use +# +# This callback object must be initialised and is used by the Diff::LCS#diff method. +# +# ```ruby +# cbo = Diff::LCS::DiffCallbacks.new +# Diff::LCS.lcs(seq1, seq2, cbo) +# cbo.finish +# ``` +# +# Note that the call to #finish is absolutely necessary, or the last set of changes will +# not be visible. This callback also supports a block mode which automatically calls +# #finish. +# +# ```ruby +# result = Diff::LCS::DiffCallbacks.new { |cbo| Diff::LCS.lcs(seq1, seq2, cbo) } +# ``` +# +# ### Simplified Array Format +# +# The simplified array format used in the example above can be obtained with: +# +# ```ruby +# require 'pp' +# pp diffs.map { |e| e.map { |f| f.to_a } } +# ``` class Diff::LCS::DiffCallbacks # Returns the difference set collected during the diff process. attr_reader :diffs @@ -122,15 +111,11 @@ def initialize # :yields: self end end - # Finalizes the diff process. If an unprocessed hunk still exists, then it - # is appended to the diff list. - def finish - finish_hunk - end + # Finalizes the diff process. If an unprocessed hunk still exists, then it is appended + # to the diff list. + def finish = finish_hunk - def match(_event) - finish_hunk - end + def match(_event) = finish_hunk def discard_a(event) @hunk << Diff::LCS::Change.new("-", event.old_position, event.old_element) @@ -147,81 +132,89 @@ def finish_hunk private :finish_hunk end -# This will produce a compound array of contextual diff change objects. Each -# element in the #diffs array is a "hunk" array, where each element in each -# "hunk" array is a single change. Each change is a Diff::LCS::ContextChange -# that contains both the old index and new index values for the change. The -# "hunk" provides the full context for the changes. Both old and new objects -# will be presented for changed objects. +nil+ will be substituted for a -# discarded object. -# -# seq1 = %w(a b c e h j l m n p) -# seq2 = %w(b c d e f j k l m r s t) -# -# diffs = Diff::LCS.diff(seq1, seq2, Diff::LCS::ContextDiffCallbacks) -# # This example shows a simplified array format. -# # [ [ [ '-', [ 0, 'a' ], [ 0, nil ] ] ], # 1 -# # [ [ '+', [ 3, nil ], [ 2, 'd' ] ] ], # 2 -# # [ [ '-', [ 4, 'h' ], [ 4, nil ] ], # 3 -# # [ '+', [ 5, nil ], [ 4, 'f' ] ] ], -# # [ [ '+', [ 6, nil ], [ 6, 'k' ] ] ], # 4 -# # [ [ '-', [ 8, 'n' ], [ 9, nil ] ], # 5 -# # [ '+', [ 9, nil ], [ 9, 'r' ] ], -# # [ '-', [ 9, 'p' ], [ 10, nil ] ], -# # [ '+', [ 10, nil ], [ 10, 's' ] ], -# # [ '+', [ 10, nil ], [ 11, 't' ] ] ] ] -# -# The five hunks shown are comprised of individual changes; if there is a -# related set of changes, they are still shown individually. -# -# This callback can also be used with Diff::LCS#sdiff, which will produce -# results like: -# -# diffs = Diff::LCS.sdiff(seq1, seq2, Diff::LCS::ContextCallbacks) -# # This example shows a simplified array format. -# # [ [ [ "-", [ 0, "a" ], [ 0, nil ] ] ], # 1 -# # [ [ "+", [ 3, nil ], [ 2, "d" ] ] ], # 2 -# # [ [ "!", [ 4, "h" ], [ 4, "f" ] ] ], # 3 -# # [ [ "+", [ 6, nil ], [ 6, "k" ] ] ], # 4 -# # [ [ "!", [ 8, "n" ], [ 9, "r" ] ], # 5 -# # [ "!", [ 9, "p" ], [ 10, "s" ] ], -# # [ "+", [ 10, nil ], [ 11, "t" ] ] ] ] -# -# The five hunks are still present, but are significantly shorter in total -# presentation, because changed items are shown as changes ("!") instead of -# potentially "mismatched" pairs of additions and deletions. -# -# The result of this operation is similar to that of -# Diff::LCS::SDiffCallbacks. They may be compared as: -# -# s = Diff::LCS.sdiff(seq1, seq2).reject { |e| e.action == "=" } -# c = Diff::LCS.sdiff(seq1, seq2, Diff::LCS::ContextDiffCallbacks).flatten(1) -# -# s == c # -> true -# -# === Use -# -# This callback object must be initialised and can be used by the -# Diff::LCS#diff or Diff::LCS#sdiff methods. -# -# cbo = Diff::LCS::ContextDiffCallbacks.new -# Diff::LCS.LCS(seq1, seq2, cbo) -# cbo.finish -# -# Note that the call to #finish is absolutely necessary, or the last set of -# changes will not be visible. Alternatively, can be used as: -# -# cbo = Diff::LCS::ContextDiffCallbacks.new { |tcbo| Diff::LCS.LCS(seq1, seq2, tcbo) } -# -# The necessary #finish call will be made. -# -# === Simplified Array Format -# -# The simplified array format used in the example above can be obtained -# with: -# -# require 'pp' -# pp diffs.map { |e| e.map { |f| f.to_a } } +# This will produce a compound array of contextual diff change objects. Each element in +# the #diffs array is a "hunk" array, where each element in each "hunk" array is a single +# change. Each change is a Diff::LCS::ContextChange that contains both the old index and +# new index values for the change. The "hunk" provides the full context for the changes. +# Both old and new objects will be presented for changed objects. `nil` will be +# substituted for a discarded object. +# +# ```ruby +# seq1 = %w(a b c e h j l m n p) +# seq2 = %w(b c d e f j k l m r s t) +# +# diffs = Diff::LCS.diff(seq1, seq2, Diff::LCS::ContextDiffCallbacks) +# # This example shows a simplified array format. +# # [ [ [ '-', [ 0, 'a' ], [ 0, nil ] ] ], # 1 +# # [ [ '+', [ 3, nil ], [ 2, 'd' ] ] ], # 2 +# # [ [ '-', [ 4, 'h' ], [ 4, nil ] ], # 3 +# # [ '+', [ 5, nil ], [ 4, 'f' ] ] ], +# # [ [ '+', [ 6, nil ], [ 6, 'k' ] ] ], # 4 +# # [ [ '-', [ 8, 'n' ], [ 9, nil ] ], # 5 +# # [ '+', [ 9, nil ], [ 9, 'r' ] ], +# # [ '-', [ 9, 'p' ], [ 10, nil ] ], +# # [ '+', [ 10, nil ], [ 10, 's' ] ], +# # [ '+', [ 10, nil ], [ 11, 't' ] ] ] ] +# ``` +# +# The five hunks shown are comprised of individual changes; if there is a related set of +# changes, they are still shown individually. +# +# This callback can also be used with Diff::LCS#sdiff, which will produce results like: +# +# ```ruby +# diffs = Diff::LCS.sdiff(seq1, seq2, Diff::LCS::ContextCallbacks) +# # This example shows a simplified array format. +# # [ [ [ "-", [ 0, "a" ], [ 0, nil ] ] ], # 1 +# # [ [ "+", [ 3, nil ], [ 2, "d" ] ] ], # 2 +# # [ [ "!", [ 4, "h" ], [ 4, "f" ] ] ], # 3 +# # [ [ "+", [ 6, nil ], [ 6, "k" ] ] ], # 4 +# # [ [ "!", [ 8, "n" ], [ 9, "r" ] ], # 5 +# # [ "!", [ 9, "p" ], [ 10, "s" ] ], +# # [ "+", [ 10, nil ], [ 11, "t" ] ] ] ] +# ``` +# +# The five hunks are still present, but are significantly shorter in total presentation, +# because changed items are shown as changes ("!") instead of potentially "mismatched" +# pairs of additions and deletions. +# +# The result of this operation is similar to that of Diff::LCS::SDiffCallbacks. They may +# be compared as: +# +# ```ruby +# s = Diff::LCS.sdiff(seq1, seq2).reject { |e| e.action == "=" } +# c = Diff::LCS.sdiff(seq1, seq2, Diff::LCS::ContextDiffCallbacks).flatten(1) +# +# s == c # => true +# ``` +# +# ### Use +# +# This callback object must be initialised and can be used by the Diff::LCS#diff or +# Diff::LCS#sdiff methods. +# +# ```ruby +# cbo = Diff::LCS::ContextDiffCallbacks.new +# Diff::LCS.lcs(seq1, seq2, cbo) +# cbo.finish +# ``` +# +# Note that the call to #finish is absolutely necessary, or the last set of changes will +# not be visible. This callback also supports a block mode which automatically calls +# #finish. +# +# ```ruby +# result = Diff::LCS::ContextDiffCallbacks.new { |cbo| Diff::LCS.lcs(seq1, seq2, cbo) } +# ``` +# +# ### Simplified Array Format +# +# The simplified array format used in the example above can be obtained with: +# +# ```ruby +# require 'pp' +# pp diffs.map { |e| e.map { |f| f.to_a } } +# ``` class Diff::LCS::ContextDiffCallbacks < Diff::LCS::DiffCallbacks def discard_a(event) @hunk << Diff::LCS::ContextChange.simplify(event) @@ -236,70 +229,75 @@ def change(event) end end -# This will produce a simple array of diff change objects. Each element in -# the #diffs array is a single ContextChange. In the set of #diffs provided -# by SDiffCallbacks, both old and new objects will be presented for both -# changed and unchanged objects. +nil+ will be substituted -# for a discarded object. -# -# The diffset produced by this callback, when provided to Diff::LCS#sdiff, -# will compute and display the necessary components to show two sequences -# and their minimized differences side by side, just like the Unix utility -# +sdiff+. -# -# same same -# before | after -# old < - -# - > new -# -# seq1 = %w(a b c e h j l m n p) -# seq2 = %w(b c d e f j k l m r s t) -# -# diffs = Diff::LCS.sdiff(seq1, seq2) -# # This example shows a simplified array format. -# # [ [ "-", [ 0, "a"], [ 0, nil ] ], -# # [ "=", [ 1, "b"], [ 0, "b" ] ], -# # [ "=", [ 2, "c"], [ 1, "c" ] ], -# # [ "+", [ 3, nil], [ 2, "d" ] ], -# # [ "=", [ 3, "e"], [ 3, "e" ] ], -# # [ "!", [ 4, "h"], [ 4, "f" ] ], -# # [ "=", [ 5, "j"], [ 5, "j" ] ], -# # [ "+", [ 6, nil], [ 6, "k" ] ], -# # [ "=", [ 6, "l"], [ 7, "l" ] ], -# # [ "=", [ 7, "m"], [ 8, "m" ] ], -# # [ "!", [ 8, "n"], [ 9, "r" ] ], -# # [ "!", [ 9, "p"], [ 10, "s" ] ], -# # [ "+", [ 10, nil], [ 11, "t" ] ] ] -# -# The result of this operation is similar to that of -# Diff::LCS::ContextDiffCallbacks. They may be compared as: -# -# s = Diff::LCS.sdiff(seq1, seq2).reject { |e| e.action == "=" } -# c = Diff::LCS.sdiff(seq1, seq2, Diff::LCS::ContextDiffCallbacks).flatten(1) -# -# s == c # -> true -# -# === Use -# -# This callback object must be initialised and is used by the Diff::LCS#sdiff -# method. -# -# cbo = Diff::LCS::SDiffCallbacks.new -# Diff::LCS.LCS(seq1, seq2, cbo) -# -# As with the other initialisable callback objects, -# Diff::LCS::SDiffCallbacks can be initialised with a block. As there is no -# "fininishing" to be done, this has no effect on the state of the object. -# -# cbo = Diff::LCS::SDiffCallbacks.new { |tcbo| Diff::LCS.LCS(seq1, seq2, tcbo) } -# -# === Simplified Array Format -# -# The simplified array format used in the example above can be obtained -# with: -# -# require 'pp' -# pp diffs.map { |e| e.to_a } +# This will produce a simple array of diff change objects. Each element in the #diffs +# array is a single ContextChange. In the set of #diffs provided by SDiffCallbacks, both +# old and new objects will be presented for both changed and unchanged +# objects. `nil` will be substituted for a discarded object. +# +# The diffset produced by this callback, when provided to Diff::LCS#sdiff, will compute +# and display the necessary components to show two sequences and their minimized +# differences side by side, just like the Unix utility `sdiff`. +# +# ```ruby +# # same same +# # before | after +# # old < - +# # - > new +# +# seq1 = %w(a b c e h j l m n p) +# seq2 = %w(b c d e f j k l m r s t) +# +# diffs = Diff::LCS.sdiff(seq1, seq2) +# # This example shows a simplified array format. +# # [ [ "-", [ 0, "a"], [ 0, nil ] ], +# # [ "=", [ 1, "b"], [ 0, "b" ] ], +# # [ "=", [ 2, "c"], [ 1, "c" ] ], +# # [ "+", [ 3, nil], [ 2, "d" ] ], +# # [ "=", [ 3, "e"], [ 3, "e" ] ], +# # [ "!", [ 4, "h"], [ 4, "f" ] ], +# # [ "=", [ 5, "j"], [ 5, "j" ] ], +# # [ "+", [ 6, nil], [ 6, "k" ] ], +# # [ "=", [ 6, "l"], [ 7, "l" ] ], +# # [ "=", [ 7, "m"], [ 8, "m" ] ], +# # [ "!", [ 8, "n"], [ 9, "r" ] ], +# # [ "!", [ 9, "p"], [ 10, "s" ] ], +# # [ "+", [ 10, nil], [ 11, "t" ] ] ] +# ``` +# +# The result of this operation is similar to that of Diff::LCS::ContextDiffCallbacks. They +# may be compared as: +# +# ```ruby +# s = Diff::LCS.sdiff(seq1, seq2).reject { |e| e.action == "=" } +# c = Diff::LCS.sdiff(seq1, seq2, Diff::LCS::ContextDiffCallbacks).flatten(1) +# +# s == c # => true +# ``` +# +# ### Use +# +# This callback object must be initialised and is used by the Diff::LCS#sdiff method. +# +# ```ruby +# cbo = Diff::LCS::SDiffCallbacks.new +# Diff::LCS.lcs(seq1, seq2, cbo) +# ``` +# +# This callback also supports initialization with a block, but as there is no "finishing" +# to be done, this has no effect on the state of the object. +# +# ```ruby +# result = Diff::LCS::SDiffCallbacks.new { |cbo| Diff::LCS.lcs(seq1, seq2, cbo) } +# ``` +# +# ### Simplified Array Format +# +# The simplified array format used in the example above can be obtained with: +# +# ```ruby +# require 'pp' +# pp diffs.map { |e| e.to_a } +# ``` class Diff::LCS::SDiffCallbacks # Returns the difference set collected during the diff process. attr_reader :diffs diff --git a/lib/diff/lcs/change.rb b/lib/diff/lcs/change.rb index 714d78c8..99ddb5e2 100644 --- a/lib/diff/lcs/change.rb +++ b/lib/diff/lcs/change.rb @@ -1,53 +1,49 @@ # frozen_string_literal: true -# Represents a simplistic (non-contextual) change. Represents the removal or -# addition of an element from either the old or the new sequenced -# enumerable. -class Diff::LCS::Change - IntClass = 1.class # Fixnum is deprecated in Ruby 2.4 # standard:disable Naming/ConstantName +Diff::LCS::Change = Data.define(:action, :position, :element) # :nodoc: +Diff::LCS::ContextChange = Data.define(:action, :old_position, :old_element, :new_position, :new_element) # :nodoc: - # The only actions valid for changes are '+' (add), '-' (delete), '=' - # (no change), '!' (changed), '<' (tail changes from first sequence), or - # '>' (tail changes from second sequence). The last two ('<>') are only - # found with Diff::LCS::diff and Diff::LCS::sdiff. +# Represents a simplistic (non-contextual) change. Represents the removal or addition of +# an element from either the old or the new sequenced enumerable. +class Diff::LCS::Change + # The only actions valid for changes are '+' (add), '-' (delete), '=' (no change), '!' + # (changed), '<' (tail changes from first sequence), or '>' (tail changes from second + # sequence). The last two ('<>') are only found with Diff::LCS::diff and + # Diff::LCS::sdiff. VALID_ACTIONS = %w[+ - = ! > <].freeze - def self.valid_action?(action) - VALID_ACTIONS.include? action - end + def self.valid_action?(action) = VALID_ACTIONS.include?(action) + ## # Returns the action this Change represents. - attr_reader :action + # :attr_reader: action + ## # Returns the position of the Change. - attr_reader :position - # Returns the sequence element of the Change. - attr_reader :element + # :attr_reader: position - def initialize(*args) - @action, @position, @element = *args + ## + # Returns the sequence element of the Change. + # :attr_reader: element - fail "Invalid Change Action '#{@action}'" unless Diff::LCS::Change.valid_action?(@action) - fail "Invalid Position Type" unless @position.is_a? IntClass - end + def initialize(action:, position:, element:) + fail "Invalid Change Action '#{action}'" unless Diff::LCS::Change.valid_action?(action) + fail "Invalid Position Type" unless position.is_a?(Integer) - def inspect(*_args) - "#<#{self.class}: #{to_a.inspect}>" + super end - def to_a - [@action, @position, @element] - end + def inspect(*_args) = "#<#{self.class}: #{to_a.inspect}>" + def to_a = [action, position, element] alias_method :to_ary, :to_a def self.from_a(arr) - arr = arr.flatten(1) - case arr.size - when 5 - Diff::LCS::ContextChange.new(*arr[0...5]) - when 3 - Diff::LCS::Change.new(*arr[0...3]) + case arr + in [action, [old_position, old_element], [new_position, new_element]] + Diff::LCS::ContextChange[action, old_position, old_element, new_position, new_element] + in [action, position, element] + new(action, position, element) else fail "Invalid change array format provided." end @@ -69,72 +65,57 @@ def <=>(other) r end - def adding? - @action == "+" - end + def adding? = action == "+" - def deleting? - @action == "-" - end + def deleting? = action == "-" - def unchanged? - @action == "=" - end + def unchanged? = action == "=" - def changed? - @action == "!" - end + def changed? = action == "!" - def finished_a? - @action == ">" - end + def finished_a? = action == ">" - def finished_b? - @action == "<" - end + def finished_b? = action == "<" end -# Represents a contextual change. Contains the position and values of the -# elements in the old and the new sequenced enumerables as well as the action -# taken. -class Diff::LCS::ContextChange < Diff::LCS::Change - # We don't need these two values. - undef :position - undef :element +# Represents a contextual change. Contains the position and values of the elements in the +# old and the new sequenced enumerable values as well as the action taken. +class Diff::LCS::ContextChange + ## + # Returns the action this Change represents. + # :attr_reader: action + ## # Returns the old position being changed. - attr_reader :old_position + # :attr_reader: old_position + + ## # Returns the new position being changed. - attr_reader :new_position + # :attr_reader: new_position + + ## # Returns the old element being changed. - attr_reader :old_element - # Returns the new element being changed. - attr_reader :new_element + # :attr_reader: old_element - def initialize(*args) - @action, @old_position, @old_element, @new_position, @new_element = *args + ## + # Returns the new element being changed. + # :attr_reader: new_element - fail "Invalid Change Action '#{@action}'" unless Diff::LCS::Change.valid_action?(@action) - fail "Invalid (Old) Position Type" unless @old_position.nil? || @old_position.is_a?(IntClass) - fail "Invalid (New) Position Type" unless @new_position.nil? || @new_position.is_a?(IntClass) - end + def initialize(action:, old_position:, old_element:, new_position:, new_element:) + fail "Invalid Change Action '#{action}'" unless Diff::LCS::Change.valid_action?(action) + fail "Invalid (Old) Position Type" unless old_position.nil? || old_position.is_a?(Integer) + fail "Invalid (New) Position Type" unless new_position.nil? || new_position.is_a?(Integer) - def to_a - [ - @action, - [@old_position, @old_element], - [@new_position, @new_element] - ] + super end + def to_a = [action, [old_position, old_element], [new_position, new_element]] alias_method :to_ary, :to_a - def self.from_a(arr) - Diff::LCS::Change.from_a(arr) - end + def self.from_a(arr) = Diff::LCS::Change.from_a(arr) - # Simplifies a context change for use in some diff callbacks. '<' actions - # are converted to '-' and '>' actions are converted to '+'. + # Simplifies a context change for use in some diff callbacks. '<' actions are converted + # to '-' and '>' actions are converted to '+'. def self.simplify(event) ea = event.to_a @@ -151,24 +132,36 @@ def self.simplify(event) ea[1][1] = nil end - Diff::LCS::ContextChange.from_a(ea) + from_a(ea) end def ==(other) - (self.class == other.class) and - (@action == other.action) and - (@old_position == other.old_position) and - (@new_position == other.new_position) and - (@old_element == other.old_element) and - (@new_element == other.new_element) + (self.class == other.class) && + (action == other.action) && + (old_position == other.old_position) && + (new_position == other.new_position) && + (old_element == other.old_element) && + (new_element == other.new_element) end def <=>(other) - r = @action <=> other.action - r = @old_position <=> other.old_position if r.zero? - r = @new_position <=> other.new_position if r.zero? - r = @old_element <=> other.old_element if r.zero? - r = @new_element <=> other.new_element if r.zero? + r = action <=> other.action + r = old_position <=> other.old_position if r.zero? + r = new_position <=> other.new_position if r.zero? + r = old_element <=> other.old_element if r.zero? + r = new_element <=> other.new_element if r.zero? r end + + def adding? = action == "+" + + def deleting? = action == "-" + + def unchanged? = action == "=" + + def changed? = action == "!" + + def finished_a? = action == ">" + + def finished_b? = action == "<" end diff --git a/lib/diff/lcs/htmldiff.rb b/lib/diff/lcs/htmldiff.rb deleted file mode 100644 index 90732438..00000000 --- a/lib/diff/lcs/htmldiff.rb +++ /dev/null @@ -1,160 +0,0 @@ -# frozen_string_literal: true - -require "erb" - -# Produce a simple HTML diff view. -class Diff::LCS::HTMLDiff - class << self - # standard:disable ThreadSafety/ClassAndModuleAttributes - attr_accessor :can_expand_tabs # :nodoc: - # standard:enable ThreadSafety/ClassAndModuleAttributes - end - self.can_expand_tabs = true - - class Callbacks # :nodoc: - attr_accessor :output - attr_accessor :match_class - attr_accessor :only_a_class - attr_accessor :only_b_class - - def initialize(output, options = {}) - @output = output - options ||= {} - - @match_class = options[:match_class] || "match" - @only_a_class = options[:only_a_class] || "only_a" - @only_b_class = options[:only_b_class] || "only_b" - end - - def htmlize(element, css_class) - element = " " if element.empty? - %(
#{element}
\n) - end - private :htmlize - - # This will be called with both lines are the same - def match(event) - @output << htmlize(event.old_element, :match_class) - end - - # This will be called when there is a line in A that isn't in B - def discard_a(event) - @output << htmlize(event.old_element, :only_a_class) - end - - # This will be called when there is a line in B that isn't in A - def discard_b(event) - @output << htmlize(event.new_element, :only_b_class) - end - end - - # standard:disable Style/HashSyntax - DEFAULT_OPTIONS = { - :expand_tabs => nil, - :output => nil, - :css => nil, - :title => nil - }.freeze - # standard:enable Style/HashSyntax - - # standard:disable Layout/HeredocIndentation - DEFAULT_CSS = <<-CSS -body { margin: 0; } -.diff -{ - border: 1px solid black; - margin: 1em 2em; -} -p -{ - margin-left: 2em; -} -pre -{ - padding-left: 1em; - margin: 0; - font-family: Inconsolata, Consolas, Lucida, Courier, monospaced; - white-space: pre; -} -.match { } -.only_a -{ - background-color: #fdd; - color: red; - text-decoration: line-through; -} -.only_b -{ - background-color: #ddf; - color: blue; - border-left: 3px solid blue -} -h1 { margin-left: 2em; } - CSS - # standard:enable Layout/HeredocIndentation - - def initialize(left, right, options = nil) - @left = left - @right = right - @options = options - - @options = DEFAULT_OPTIONS.dup if @options.nil? - end - - def verify_options - @options[:expand_tabs] ||= 4 - @options[:expand_tabs] = 4 if @options[:expand_tabs].negative? - - @options[:output] ||= $stdout - - @options[:css] ||= DEFAULT_CSS.dup - - @options[:title] ||= "diff" - end - private :verify_options - - attr_reader :options - - def run - verify_options - - if @options[:expand_tabs].positive? && self.class.can_expand_tabs - formatter = Text::Format.new - formatter.tabstop = @options[:expand_tabs] - - @left.map! { |line| formatter.expand(line.chomp) } - @right.map! { |line| formatter.expand(line.chomp) } - end - - @left.map! { |line| ERB::Util.html_escape(line.chomp) } - @right.map! { |line| ERB::Util.html_escape(line.chomp) } - - # standard:disable Layout/HeredocIndentation - @options[:output] << <<-OUTPUT - - - #{@options[:title]} - - - -

#{@options[:title]}

-

Legend: Only in Old  - Only in New

-
- OUTPUT - # standard:enable Layout/HeredocIndentation - - callbacks = Callbacks.new(@options[:output]) - Diff::LCS.traverse_sequences(@left, @right, callbacks) - - # standard:disable Layout/HeredocIndentation - @options[:output] << <<-OUTPUT -
- - - OUTPUT - # standard:enable Layout/HeredocIndentation - end -end diff --git a/lib/diff/lcs/hunk.rb b/lib/diff/lcs/hunk.rb index 24b33bca..cd42fdfa 100644 --- a/lib/diff/lcs/hunk.rb +++ b/lib/diff/lcs/hunk.rb @@ -2,28 +2,26 @@ require "diff/lcs/block" -# A Hunk is a group of Blocks which overlap because of the context surrounding -# each block. (So if we're not using context, every hunk will contain one -# block.) Used in the diff program (bin/ldiff). +# A Hunk is a group of Blocks which overlap because of the context surrounding each block. +# (So if we're not using context, every hunk will contain one block.) Used in the diff +# program (bin/ldiff). class Diff::LCS::Hunk OLD_DIFF_OP_ACTION = {"+" => "a", "-" => "d", "!" => "c"}.freeze # :nodoc: - ED_DIFF_OP_ACTION = {"+" => "a", "-" => "d", "!" => "c"}.freeze # :nodoc: + private_constant :OLD_DIFF_OP_ACTION - private_constant :OLD_DIFF_OP_ACTION, :ED_DIFF_OP_ACTION if respond_to?(:private_constant) - - # Create a hunk using references to both the old and new data, as well as the - # piece of data. + # Create a hunk using references to both the old and new data, as well as the piece of + # data. def initialize(data_old, data_new, piece, flag_context, file_length_difference) # At first, a hunk will have just one Block in it - @blocks = [Diff::LCS::Block.new(piece)] + @blocks = [Diff::LCS::Block.from_chunk(piece)] if @blocks[0].remove.empty? && @blocks[0].insert.empty? fail "Cannot build a hunk from #{piece.inspect}; has no add or remove actions" end - if String.method_defined?(:encoding) - @preferred_data_encoding = data_old.fetch(0) { data_new.fetch(0) { "" } }.encoding - end + @preferred_data_encoding = data_old.fetch(0) { data_new.fetch(0) { "" } }.encoding + @newline = "\n".encode(@preferred_data_encoding) + @missing_newline = "\\ No newline at end of file".encode(@preferred_data_encoding) @data_old = data_old @data_new = data_new @@ -35,10 +33,9 @@ def initialize(data_old, data_new, piece, flag_context, file_length_difference) @file_length_difference = after # The caller must get this manually @max_diff_size = @blocks.map { |e| e.diff_size.abs }.max - # Save the start & end of each array. If the array doesn't exist (e.g., - # we're only adding items in this block), then figure out the line number - # based on the line number of the other file and the current difference in - # file lengths. + # Save the start and end of each array. If the array doesn't exist (e.g., we're only + # adding items in this block), then figure out the line number based on the line + # number of the other file and the current difference in file lengths. if @blocks[0].remove.empty? a1 = a2 = nil else @@ -66,11 +63,13 @@ def initialize(data_old, data_new, piece, flag_context, file_length_difference) attr_reader :end_old, :end_new attr_reader :file_length_difference - # Change the "start" and "end" fields to note that context should be added - # to this hunk. - attr_accessor :flag_context - undef :flag_context= - def flag_context=(context) # :nodoc: # standard:disable Lint/DuplicateMethods + ## + # Change the "start" and "end" fields to note that context should be added to this hunk. + # :attr_accessor: :flag_context + attr_reader :flag_context + + ## + def flag_context=(context) # :nodoc: return if context.nil? || context.zero? add_start = (context > @start_old) ? @start_old : context @@ -91,9 +90,8 @@ def flag_context=(context) # :nodoc: # standard:disable Lint/DuplicateMethods @end_new += add_end end - # Merges this hunk and the provided hunk together if they overlap. Returns - # a truthy value so that if there is no overlap, you can know the merge - # was skipped. + # Merges this hunk and the provided hunk together if they overlap. Returns a truthy + # value so that if there is no overlap, you can know the merge was skipped. def merge(hunk) return unless overlaps?(hunk) @@ -103,12 +101,11 @@ def merge(hunk) end alias_method :unshift, :merge - # Determines whether there is an overlap between this hunk and the - # provided hunk. This will be true if the difference between the two hunks - # start or end positions is within one position of each other. + # Determines whether there is an overlap between this hunk and the provided hunk. This + # will be true if the difference between the two hunks start or end positions is within + # one position of each other. def overlaps?(hunk) - hunk and (((@start_old - hunk.end_old) <= 1) or - ((@start_new - hunk.end_new) <= 1)) + hunk && (((@start_old - hunk.end_old) <= 1) || ((@start_new - hunk.end_new) <= 1)) end # Returns a diff string based on a format. @@ -120,20 +117,21 @@ def diff(format, last = false) unified_diff(last) when :context context_diff(last) - when :ed - self - when :reverse_ed, :ed_finish - ed_diff(format, last) else fail "Unknown diff format #{format}." end end - # Note that an old diff can't have any context. Therefore, we know that - # there's only one block in the hunk. + private + + # Note that an old diff can't have any context. Therefore, we know that there's only one + # block in the hunk. def old_diff(last = false) warn "Expecting only one block in an old diff hunk!" if @blocks.size > 1 + del, ins, sep, _ = ["< ", "> ", "---\n", "\\ No newline at end of file\n"] + .map { _1.encode(@preferred_data_encoding) } + block = @blocks[0] if last @@ -141,47 +139,45 @@ def old_diff(last = false) new_missing_newline = !@new_empty && missing_last_newline?(@data_new) end - # Calculate item number range. Old diff range is just like a context - # diff range, except the ranges are on one line with the action between - # them. - s = encode("#{context_range(:old, ",")}#{OLD_DIFF_OP_ACTION[block.op]}#{context_range(:new, ",")}\n") - # If removing anything, just print out all the remove lines in the hunk - # which is just all the remove lines in the block. + # Calculate item number range. Old diff range is just like a context diff range, + # except the ranges are on one line with the action between them. + s = "#{context_range(:old, ",")}#{OLD_DIFF_OP_ACTION[block.op]}#{context_range(:new, ",")}\n" + .encode(@preferred_data_encoding) + # If removing anything, just print out all the remove lines in the hunk which is just + # all the remove lines in the block. unless block.remove.empty? - @data_old[@start_old..@end_old].each { |e| s << encode("< ") + e.chomp + encode("\n") } + @data_old[@start_old..@end_old].each { |e| s << del + e.chomp + @newline } end - s << encode("\\ No newline at end of file\n") if old_missing_newline && !new_missing_newline - s << encode("---\n") if block.op == "!" + s << @missing_newline << @newline if old_missing_newline && !new_missing_newline + s << sep if block.op == "!" unless block.insert.empty? - @data_new[@start_new..@end_new].each { |e| s << encode("> ") + e.chomp + encode("\n") } + @data_new[@start_new..@end_new].each { |e| s << ins + e.chomp + @newline } end - s << encode("\\ No newline at end of file\n") if new_missing_newline && !old_missing_newline + s << @missing_newline << @newline if new_missing_newline && !old_missing_newline s end - private :old_diff def unified_diff(last = false) # Calculate item number range. - s = encode("@@ -#{unified_range(:old)} +#{unified_range(:new)} @@\n") + s = "@@ -#{unified_range(:old)} +#{unified_range(:new)} @@\n" + .encode(@preferred_data_encoding) - # Outlist starts containing the hunk of the old file. Removing an item - # just means putting a '-' in front of it. Inserting an item requires - # getting it from the new file and splicing it in. We splice in - # +num_added+ items. Remove blocks use +num_added+ because splicing - # changed the length of outlist. + # `outlist` starts containing the hunk of the old file. Removing an item just means + # putting a '-' in front of it. Inserting an item requires getting it from the new + # file and splicing it in. We splice in `num_added` items. Remove blocks use + # `num_added` because splicing changed the length of outlist. # - # We remove +num_removed+ items. Insert blocks use +num_removed+ - # because their item numbers -- corresponding to positions in the NEW - # file -- don't take removed items into account. + # We remove `num_removed` items. Insert blocks use `num_removed` because their item + # numbers -- corresponding to positions in the NEW file -- don't take removed items + # into account. lo, hi, num_added, num_removed = @start_old, @end_old, 0, 0 - # standard:disable Performance/UnfreezeString - outlist = @data_old[lo..hi].map { |e| String.new("#{encode(" ")}#{e.chomp}") } - # standard:enable Performance/UnfreezeString + space = " ".encode(@preferred_data_encoding) + outlist = @data_old[lo..hi].map { |e| "#{space}#{e.chomp}" } last_block = blocks[-1] @@ -192,127 +188,92 @@ def unified_diff(last = false) @blocks.each do |block| block.remove.each do |item| - op = item.action.to_s # - offset = item.position - lo + num_added - outlist[offset][0, 1] = encode(op) + outlist[offset][0, 1] = item.action.to_s.encode(@preferred_data_encoding) # - num_removed += 1 end if last && block == last_block && old_missing_newline && !new_missing_newline - outlist << encode('\\ No newline at end of file') + outlist << @missing_newline num_removed += 1 end block.insert.each do |item| - op = item.action.to_s # + + op = item.action.to_s.encode(@preferred_data_encoding) # + offset = item.position - @start_new + num_removed - outlist[offset, 0] = encode(op) + @data_new[item.position].chomp + outlist[offset, 0] = op + @data_new[item.position].chomp num_added += 1 end end - outlist << encode('\\ No newline at end of file') if last && new_missing_newline + outlist << @missing_newline if last && new_missing_newline - s << outlist.join(encode("\n")) + s << outlist.join("\n".encode(@preferred_data_encoding)) s end - private :unified_diff def context_diff(last = false) - s = encode("***************\n") - s << encode("*** #{context_range(:old, ",")} ****\n") + s = "***************\n".encode(@preferred_data_encoding) + s << "*** #{context_range(:old, ",")} ****\n".encode(@preferred_data_encoding) r = context_range(:new, ",") + spaces = " ".encode(@preferred_data_encoding) + if last old_missing_newline = missing_last_newline?(@data_old) new_missing_newline = missing_last_newline?(@data_new) end - # Print out file 1 part for each block in context diff format if there - # are any blocks that remove items + # Print out file 1 part for each block in context diff format if there are any blocks + # that remove items lo, hi = @start_old, @end_old removes = @blocks.reject { |e| e.remove.empty? } unless removes.empty? - # standard:disable Performance/UnfreezeString - outlist = @data_old[lo..hi].map { |e| String.new("#{encode(" ")}#{e.chomp}") } - # standard:enable Performance/UnfreezeString + outlist = @data_old[lo..hi].map { |e| "#{spaces}#{e.chomp}" } last_block = removes[-1] removes.each do |block| block.remove.each do |item| - outlist[item.position - lo][0, 1] = encode(block.op) # - or ! + outlist[item.position - lo][0, 1] = block.op.encode(@preferred_data_encoding) # - or ! end if last && block == last_block && old_missing_newline - outlist << encode('\\ No newline at end of file') + outlist << @missing_newline end end - s << outlist.join(encode("\n")) << encode("\n") + s << outlist.join(@newline) << @newline end - s << encode("--- #{r} ----\n") + s << "--- #{r} ----\n".encode(@preferred_data_encoding) lo, hi = @start_new, @end_new inserts = @blocks.reject { |e| e.insert.empty? } unless inserts.empty? - # standard:disable Performance/UnfreezeString - outlist = @data_new[lo..hi].map { |e| String.new("#{encode(" ")}#{e.chomp}") } - # standard:enable Performance/UnfreezeString + outlist = @data_new[lo..hi].map { |e| "#{spaces}#{e.chomp}" } last_block = inserts[-1] inserts.each do |block| block.insert.each do |item| - outlist[item.position - lo][0, 1] = encode(block.op) # + or ! + outlist[item.position - lo][0, 1] = block.op.encode(@preferred_data_encoding) # + or ! end if last && block == last_block && new_missing_newline - outlist << encode('\\ No newline at end of file') + outlist << @missing_newline end end - s << outlist.join(encode("\n")) + s << outlist.join(@newline) end s end - private :context_diff - def ed_diff(format, last) - warn "Expecting only one block in an old diff hunk!" if @blocks.size > 1 - if last - # ed script doesn't support well incomplete lines - warn ": No newline at end of file\n" if !@old_empty && missing_last_newline?(@data_old) - warn ": No newline at end of file\n" if !@new_empty && missing_last_newline?(@data_new) - - if @blocks[0].op == "!" - return +"" if @blocks[0].changes[0].element == @blocks[0].changes[1].element + "\n" - return +"" if @blocks[0].changes[0].element + "\n" == @blocks[0].changes[1].element - end - end - - s = - if format == :reverse_ed - encode("#{ED_DIFF_OP_ACTION[@blocks[0].op]}#{context_range(:old, " ")}\n") - else - encode("#{context_range(:old, ",")}#{ED_DIFF_OP_ACTION[@blocks[0].op]}\n") - end - - unless @blocks[0].insert.empty? - @data_new[@start_new..@end_new].each do |e| - s << e.chomp + encode("\n") - end - s << encode(".\n") - end - s - end - private :ed_diff - - # Generate a range of item numbers to print. Only print 1 number if the - # range has only one item in it. Otherwise, it's 'start,end' + # Generate a range of item numbers to print. Only print 1 number if the range has only + # one item in it. Otherwise, it's 'start,end' def context_range(mode, op) case mode when :old @@ -323,57 +284,33 @@ def context_range(mode, op) (s < e) ? "#{s}#{op}#{e}" : e.to_s end - private :context_range - # Generate a range of item numbers to print for unified diff. Print number - # where block starts, followed by number of lines in the block - # (don't print number of lines if it's 1) + # Generate a range of item numbers to print for unified diff. Print number where block + # starts, followed by number of lines in the block (don't print number of lines if it's + # 1) def unified_range(mode) - case mode - when :old - return "0,0" if @old_empty - s, e = (@start_old + 1), (@end_old + 1) - when :new - return "0,0" if @new_empty - s, e = (@start_new + 1), (@end_new + 1) - end + s, e = + case mode + when :old + return "0,0" if @old_empty + [(@start_old + 1), (@end_old + 1)] + when :new + return "0,0" if @new_empty + [(@start_new + 1), (@end_new + 1)] + end length = e - s + 1 (length <= 1) ? e.to_s : "#{s},#{length}" end - private :unified_range def missing_last_newline?(data) - newline = encode("\n") - if data[-2] - data[-2].end_with?(newline) && !data[-1].end_with?(newline) + data[-2].end_with?(@newline) && !data[-1].end_with?(@newline) elsif data[-1] - !data[-1].end_with?(newline) + !data[-1].end_with?(@newline) else true end end - - if String.method_defined?(:encoding) - def encode(literal, target_encoding = @preferred_data_encoding) - literal.encode target_encoding - end - - def encode_as(string, *args) - args.map { |arg| arg.encode(string.encoding) } - end - else - def encode(literal, _target_encoding = nil) - literal - end - - def encode_as(_string, *args) - args - end - end - - private :encode - private :encode_as end diff --git a/lib/diff/lcs/internals.rb b/lib/diff/lcs/internals.rb index 8a9160a6..5052e274 100644 --- a/lib/diff/lcs/internals.rb +++ b/lib/diff/lcs/internals.rb @@ -1,22 +1,31 @@ # frozen_string_literal: true class << Diff::LCS + def callbacks_for(callbacks) # :nodoc: + callbacks.new + rescue + callbacks + end + private :callbacks_for + def diff_traversal(method, seq1, seq2, callbacks, &block) callbacks = callbacks_for(callbacks) + case method when :diff traverse_sequences(seq1, seq2, callbacks) when :sdiff traverse_balanced(seq1, seq2, callbacks) end + callbacks.finish if callbacks.respond_to? :finish if block callbacks.diffs.map do |hunk| if hunk.is_a? Array - hunk.map { |hunk_block| block[hunk_block] } + hunk.map { block.call(_1) } else - block[hunk] + block.call(hunk) end end else @@ -27,18 +36,16 @@ def diff_traversal(method, seq1, seq2, callbacks, &block) end module Diff::LCS::Internals # :nodoc: -end - -class << Diff::LCS::Internals - # Compute the longest common subsequence between the sequenced - # Enumerables +a+ and +b+. The result is an array whose contents is such - # that + # Compute the longest common subsequence between the sequenced enumerable values `a` and + # `b`. The result is an array whose contents is such that # - # result = Diff::LCS::Internals.lcs(a, b) - # result.each_with_index do |e, i| - # assert_equal(a[i], b[e]) unless e.nil? - # end - def lcs(a, b) + # ```ruby + # result = Diff::LCS::Internals.lcs(a, b) + # result.each_with_index do |e, i| + # assert_equal(a[i], b[e]) unless e.nil? + # end + # ``` + def self.lcs(a, b) a_start = b_start = 0 a_finish = a.size - 1 b_finish = b.size - 1 @@ -58,21 +65,20 @@ def lcs(a, b) b_finish -= 1 end - # Now, compute the equivalence classes of positions of elements. - # An explanation for how this works: https://codeforces.com/topic/92191 + # Now, compute the equivalence classes of positions of elements. An explanation for + # how this works: https://codeforces.com/topic/92191 b_matches = position_hash(b, b_start..b_finish) thresh = [] links = [] - string = a.is_a?(String) (a_start..a_finish).each do |i| - ai = string ? a[i, 1] : a[i] + ai = a[i] bm = b_matches[ai] k = nil bm.reverse_each do |j| - # Although the threshold check is not mandatory for this to work, - # it may have an optimization purpose + # Although the threshold check is not mandatory for this to work, it may have an + # optimization purpose. # An attempt to remove it: https://github.com/halostatue/diff-lcs/pull/72 # Why it is reintroduced: https://github.com/halostatue/diff-lcs/issues/78 if k && (thresh[k] > j) && (thresh[k - 1] < j) @@ -95,11 +101,10 @@ def lcs(a, b) vector end - # This method will analyze the provided patchset to provide a single-pass - # normalization (conversion of the array form of Diff::LCS::Change objects to - # the object form of same) and detection of whether the patchset represents - # changes to be made. - def analyze_patchset(patchset, depth = 0) + # This method will analyze the provided patchset to provide a single-pass normalization + # (conversion of the array form of Diff::LCS::Change objects to the object form of same) + # and detection of whether the patchset represents changes to be made. + def self.analyze_patchset(patchset, depth = 0) fail "Patchset too complex" if depth > 1 has_changes = false @@ -115,7 +120,7 @@ def analyze_patchset(patchset, depth = 0) patchset.each do |hunk| case hunk - when Diff::LCS::Change + when Diff::LCS::Change, Diff::LCS::ContextChange has_changes ||= !hunk.unchanged? new_patchset << hunk when Array @@ -137,15 +142,13 @@ def analyze_patchset(patchset, depth = 0) [has_changes, new_patchset] end - # Examine the patchset and the source to see in which direction the - # patch should be applied. + # Examine the patchset and the source to see in which direction the patch should be + # applied. # - # WARNING: By default, this examines the whole patch, so this could take - # some time. This also works better with Diff::LCS::ContextChange or - # Diff::LCS::Change as its source, as an array will cause the creation - # of one of the above. - def intuit_diff_direction(src, patchset, limit = nil) - string = src.is_a?(String) + # WARNING: By default, this examines the whole patch, so this could take some time. This + # also works better with Diff::LCS::ContextChange or Diff::LCS::Change as its source, as + # an array will cause the creation of one of the above. + def self.intuit_diff_direction(src, patchset, limit = nil) count = left_match = left_miss = right_match = right_miss = 0 patchset.each do |change| @@ -153,8 +156,8 @@ def intuit_diff_direction(src, patchset, limit = nil) case change when Diff::LCS::ContextChange - le = string ? src[change.old_position, 1] : src[change.old_position] - re = string ? src[change.new_position, 1] : src[change.new_position] + le = src[change.old_position] + re = src[change.new_position] case change.action when "-" # Remove details from the old string @@ -183,10 +186,10 @@ def intuit_diff_direction(src, patchset, limit = nil) end end when Diff::LCS::Change - # With a simplistic change, we can't tell the difference between - # the left and right on '!' actions, so we ignore those. On '=' - # actions, if there's a miss, we miss both left and right. - element = string ? src[change.position, 1] : src[change.position] + # With a simplistic change, we can't tell the difference between the left and + # right on '!' actions, so we ignore those. On '=' actions, if there's a miss, we + # miss both left and right. + element = src[change.position] case change.action when "-" @@ -235,74 +238,67 @@ def intuit_diff_direction(src, patchset, limit = nil) :patch end else - fail "The provided patchset does not appear to apply to the provided \ -enumerable as either source or destination value." + fail "The provided patchset does not appear to apply to the provided enumerable as either source or destination value." end end end - # Find the place at which +value+ would normally be inserted into the - # Enumerable. If that place is already occupied by +value+, do nothing - # and return +nil+. If the place does not exist (i.e., it is off the end - # of the Enumerable), add it to the end. Otherwise, replace the element - # at that point with +value+. It is assumed that the Enumerable's values - # are numeric. - # - # This operation preserves the sort order. - def replace_next_larger(enum, value, last_index = nil) - # Off the end? - if enum.empty? || (value > enum[-1]) - enum << value - return enum.size - 1 - end + class << self + # Find the place at which `value` would normally be inserted into the Enumerable. If + # that place is already occupied by `value`, do nothing and return `nil`. If the place + # does not exist (i.e., it is off the end of the Enumerable), add it to the end. + # Otherwise, replace the element at that point with `value`. It is assumed that the + # Enumerable's values are numeric. + # + # This operation preserves the sort order. + def replace_next_larger(enum, value, last_index = nil) + # Off the end? + if enum.empty? || (value > enum[-1]) + enum << value + return enum.size - 1 + end - # Binary search for the insertion point - last_index ||= enum.size - 1 - first_index = 0 - while first_index <= last_index - i = (first_index + last_index) >> 1 + # Binary search for the insertion point + last_index ||= enum.size - 1 + first_index = 0 + while first_index <= last_index + i = (first_index + last_index) >> 1 - found = enum[i] + found = enum[i] - return nil if value == found + return nil if value == found - if value > found - first_index = i + 1 - else - last_index = i - 1 + if value > found + first_index = i + 1 + else + last_index = i - 1 + end end - end - # The insertion point is in first_index; overwrite the next larger - # value. - enum[first_index] = value - first_index - end - private :replace_next_larger - - # If +vector+ maps the matching elements of another collection onto this - # Enumerable, compute the inverse of +vector+ that maps this Enumerable - # onto the collection. (Currently unused.) - def inverse_vector(a, vector) - inverse = a.dup - (0...vector.size).each do |i| - inverse[vector[i]] = i unless vector[i].nil? + # The insertion point is in first_index; overwrite the next larger value. + enum[first_index] = value + first_index end - inverse - end - private :inverse_vector - - # Returns a hash mapping each element of an Enumerable to the set of - # positions it occupies in the Enumerable, optionally restricted to the - # elements specified in the range of indexes specified by +interval+. - def position_hash(enum, interval) - string = enum.is_a?(String) - hash = Hash.new { |h, k| h[k] = [] } - interval.each do |i| - k = string ? enum[i, 1] : enum[i] - hash[k] << i + private :replace_next_larger + + # If `vector` maps the matching elements of another collection onto this Enumerable, + # compute the inverse of `vector` that maps this Enumerable onto the collection. + # (Currently unused.) + def inverse_vector(a, vector) + inverse = a.dup + (0...vector.size).each do + inverse[vector[_1]] = i unless vector[_1].nil? + end + inverse + end + private :inverse_vector + + # Returns a hash mapping each element of an Enumerable to the set of positions it + # occupies in the Enumerable, optionally restricted to the elements specified in the + # range of indexes specified by `interval`. + def position_hash(enum, interval) + Hash.new { |h, k| h[k] = [] }.tap { |hash| interval.each { hash[enum[_1]] << _1 } } end - hash + private :position_hash end - private :position_hash end diff --git a/lib/diff/lcs/ldiff.rb b/lib/diff/lcs/ldiff.rb index 6442c9bf..0126cc55 100644 --- a/lib/diff/lcs/ldiff.rb +++ b/lib/diff/lcs/ldiff.rb @@ -4,19 +4,17 @@ require "diff/lcs/hunk" class Diff::LCS::Ldiff # :nodoc: - # standard:disable Layout/HeredocIndentation - BANNER = <<-COPYRIGHT -ldiff #{Diff::LCS::VERSION} - Copyright 2004-2025 Austin Ziegler + BANNER = <<~COPYRIGHT + ldiff #{Diff::LCS::VERSION} + Copyright 2004-2025 Austin Ziegler - Part of Diff::LCS. - https://github.com/halostatue/diff-lcs + Part of Diff::LCS. + https://github.com/halostatue/diff-lcs - This program is free software. It may be redistributed and/or modified under - the terms of the GPL version 2 (or later), the Perl Artistic licence, or the - MIT licence. + This program is free software. It may be redistributed and/or modified under + the terms of the GPL version 2 (or later), the Perl Artistic licence, or the + MIT licence. COPYRIGHT - # standard:enable Layout/HeredocIndentation InputInfo = Struct.new(:filename, :data, :stat) do def initialize(filename) @@ -56,12 +54,6 @@ def run(args, _input = $stdin, output = $stdout, error = $stderr) # :nodoc: @format = :unified @lines = ctx || 3 end - o.on("-e", "Creates an 'ed' script to change", "oldfile to newfile.") do |_ctx| - @format = :ed - end - o.on("-f", "Creates an 'ed' script to change", "oldfile to newfile in reverse order.") do |_ctx| - @format = :reverse_ed - end o.on( "-a", "--text", "Treat the files as text and compare them", "line-by-line, even if they do not seem", "to be text." @@ -117,8 +109,8 @@ def diff?(info_old, info_new, format, output, binary: nil, lines: 0) char_new = "+" * 3 end - # After we've read up to a certain point in each file, the number of - # items we've read from each file will differ by FLD (could be 0). + # After we've read up to a certain point in each file, the number of items we've read + # from each file will differ by FLD (could be 0). file_length_difference = 0 # Test binary status @@ -155,26 +147,23 @@ def diff?(info_old, info_new, format, output, binary: nil, lines: 0) ft = info_new.stat.mtime.localtime.strftime("%Y-%m-%d %H:%M:%S.000000000 %z") output << "#{char_new} #{info_new.filename}\t#{ft}\n" when :ed - real_output = output output = [] end - # Loop over hunks. If a hunk overlaps with the last hunk, join them. - # Otherwise, print out the old one. + # Loop over hunks. If a hunk overlaps with the last hunk, join them. Otherwise, print + # out the old one. oldhunk = hunk = nil diffs.each do |piece| - begin - hunk = Diff::LCS::Hunk.new(data_old, data_new, piece, lines, file_length_difference) - file_length_difference = hunk.file_length_difference + hunk = Diff::LCS::Hunk.new(data_old, data_new, piece, lines, file_length_difference) + file_length_difference = hunk.file_length_difference - next unless oldhunk - next if lines.positive? && hunk.merge(oldhunk) + next unless oldhunk + next if lines.positive? && hunk.merge(oldhunk) - output << oldhunk.diff(format) - output << "\n" if format == :unified - ensure - oldhunk = hunk - end + output << oldhunk.diff(format) + output << "\n" if format == :unified + ensure + oldhunk = hunk end last = oldhunk.diff(format, true) @@ -182,8 +171,6 @@ def diff?(info_old, info_new, format, output, binary: nil, lines: 0) output << last - output.reverse_each { |e| real_output << e.diff(:ed_finish, e == output[0]) } if format == :ed - - true + 1 end end diff --git a/lib/diff/lcs/version.rb b/lib/diff/lcs/version.rb index 82830e3c..9796d917 100644 --- a/lib/diff/lcs/version.rb +++ b/lib/diff/lcs/version.rb @@ -2,6 +2,6 @@ module Diff module LCS - VERSION = "1.6.2" + VERSION = "2.0.0.beta.1" end end diff --git a/spec/fixtures/ldiff/output.diff-e b/spec/fixtures/ldiff/output.diff-e deleted file mode 100644 index 13e0f7f0..00000000 --- a/spec/fixtures/ldiff/output.diff-e +++ /dev/null @@ -1,3 +0,0 @@ -1c -bXaX -. diff --git a/spec/fixtures/ldiff/output.diff-f b/spec/fixtures/ldiff/output.diff-f deleted file mode 100644 index 77710c76..00000000 --- a/spec/fixtures/ldiff/output.diff-f +++ /dev/null @@ -1,3 +0,0 @@ -c1 -bXaX -. diff --git a/spec/fixtures/ldiff/output.diff.chef-e b/spec/fixtures/ldiff/output.diff.chef-e deleted file mode 100644 index 775d881c..00000000 --- a/spec/fixtures/ldiff/output.diff.chef-e +++ /dev/null @@ -1,3 +0,0 @@ -3c - "description": "lo" -. diff --git a/spec/fixtures/ldiff/output.diff.chef-f b/spec/fixtures/ldiff/output.diff.chef-f deleted file mode 100644 index 9bf1e67f..00000000 --- a/spec/fixtures/ldiff/output.diff.chef-f +++ /dev/null @@ -1,3 +0,0 @@ -c3 - "description": "lo" -. diff --git a/spec/fixtures/ldiff/output.diff.chef2-e b/spec/fixtures/ldiff/output.diff.chef2-e deleted file mode 100644 index 89f3fa07..00000000 --- a/spec/fixtures/ldiff/output.diff.chef2-e +++ /dev/null @@ -1,7 +0,0 @@ -14a -recipe[o::new] -recipe[p::new] -recipe[q::new] -recipe[r::new] -. -2d diff --git a/spec/fixtures/ldiff/output.diff.chef2-f b/spec/fixtures/ldiff/output.diff.chef2-f deleted file mode 100644 index ca32a490..00000000 --- a/spec/fixtures/ldiff/output.diff.chef2-f +++ /dev/null @@ -1,7 +0,0 @@ -d2 -a14 -recipe[o::new] -recipe[p::new] -recipe[q::new] -recipe[r::new] -. diff --git a/spec/hunk_spec.rb b/spec/hunk_spec.rb index 7d910399..e74b7bb6 100644 --- a/spec/hunk_spec.rb +++ b/spec/hunk_spec.rb @@ -56,17 +56,6 @@ expect(hunk.diff(:old)).to eq(expected) end - it "produces a reverse ed diff from the two pieces" do - expected = <<-EXPECTED.gsub(/^ +/, "").encode("UTF-16LE").chomp - c1 - Tu a un carte avec {count} items - . - - EXPECTED - - expect(hunk.diff(:reverse_ed)).to eq(expected) - end - context "with empty first data set" do let(:old_data) { [] } diff --git a/spec/lcs_spec.rb b/spec/lcs_spec.rb index c17f22f6..1b50e297 100644 --- a/spec/lcs_spec.rb +++ b/spec/lcs_spec.rb @@ -31,26 +31,26 @@ end end -describe Diff::LCS, ".LCS" do +describe Diff::LCS, ".lcs" do include Diff::LCS::SpecHelper::Matchers - it "returns the correct compacted values from Diff::LCS.LCS" do - res = Diff::LCS.LCS(seq1, seq2) + it "returns the correct compacted values from Diff::LCS.lcs" do + res = Diff::LCS.lcs(seq1, seq2) expect(res).to eq(correct_lcs) expect(res.compact).to eq(res) end it "is transitive" do - res = Diff::LCS.LCS(seq2, seq1) + res = Diff::LCS.lcs(seq2, seq1) expect(res).to eq(correct_lcs) expect(res.compact).to eq(res) end it "returns %W(h e l l o) with (hello, hello)" do - expect(Diff::LCS.LCS(hello, hello)).to eq(hello.chars) + expect(Diff::LCS.lcs(hello, hello)).to eq(hello.chars) end it "returns hello_ary with (hello_ary, hello_ary)" do - expect(Diff::LCS.LCS(hello_ary, hello_ary)).to eq(hello_ary) + expect(Diff::LCS.lcs(hello_ary, hello_ary)).to eq(hello_ary) end end diff --git a/spec/ldiff_spec.rb b/spec/ldiff_spec.rb index e13b5614..2367220c 100644 --- a/spec/ldiff_spec.rb +++ b/spec/ldiff_spec.rb @@ -17,7 +17,7 @@ {:name => "diff.bin2", :left => "file1.bin", :right => "file2.bin", :diff => 1}, {:name => "diff.chef", :left => "old-chef", :right => "new-chef", :diff => 1}, {:name => "diff.chef2", :left => "old-chef2", :right => "new-chef2", :diff => 1} - ].product([nil, "-e", "-f", "-c", "-u"]).map { |(fixture, flag)| + ].product([nil, "-c", "-u"]).map { |(fixture, flag)| fixture = fixture.dup fixture[:flag] = flag fixture diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 69696bb4..9cde3ab9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,24 +5,22 @@ require "psych" if RUBY_VERSION >= "1.9" -if ENV["COVERAGE"] == "true" - require "simplecov" - require "simplecov-lcov" +require "simplecov" +require "simplecov-lcov" - SimpleCov::Formatter::LcovFormatter.config do |config| - config.report_with_single_file = true - config.lcov_file_name = "lcov.info" - end +SimpleCov::Formatter::LcovFormatter.config do |config| + config.report_with_single_file = true + config.lcov_file_name = "lcov.info" +end - SimpleCov.start "test_frameworks" do - enable_coverage :branch - primary_coverage :branch - formatter SimpleCov::Formatter::MultiFormatter.new([ - SimpleCov::Formatter::HTMLFormatter, - SimpleCov::Formatter::LcovFormatter, - SimpleCov::Formatter::SimpleFormatter - ]) - end +SimpleCov.start "test_frameworks" do + enable_coverage :branch + primary_coverage :branch + formatter SimpleCov::Formatter::MultiFormatter.new([ + SimpleCov::Formatter::HTMLFormatter, + SimpleCov::Formatter::LcovFormatter, + SimpleCov::Formatter::SimpleFormatter + ]) end file = Pathname.new(__FILE__).expand_path