├── .fasterer.yml ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── dependency-review.yml │ ├── publish-gem.yml │ ├── reviewdog.yml │ ├── scorecards.yml │ └── zizmor.yml ├── .gitignore ├── .hoerc ├── .rspec ├── .standard.yml ├── .typos.toml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── Gemfile ├── LICENCE.md ├── Manifest.txt ├── README.md ├── Rakefile ├── SECURITY.md ├── bin ├── htmldiff └── ldiff ├── diff-lcs.gemspec ├── docs ├── COPYING.txt └── artistic.txt ├── lib ├── diff-lcs.rb └── diff │ ├── lcs.rb │ └── lcs │ ├── array.rb │ ├── backports.rb │ ├── block.rb │ ├── callbacks.rb │ ├── change.rb │ ├── htmldiff.rb │ ├── hunk.rb │ ├── internals.rb │ ├── ldiff.rb │ ├── string.rb │ └── version.rb ├── mise.toml └── spec ├── change_spec.rb ├── diff_spec.rb ├── fixtures ├── 123_x ├── 456_x ├── aX ├── bXaX ├── ds1.csv ├── ds2.csv ├── empty ├── file1.bin ├── file2.bin ├── four_lines ├── four_lines_with_missing_new_line ├── ldiff │ ├── diff.missing_new_line1-e │ ├── diff.missing_new_line1-f │ ├── diff.missing_new_line2-e │ ├── diff.missing_new_line2-f │ ├── error.diff.chef-e │ ├── error.diff.chef-f │ ├── error.diff.missing_new_line1-e │ ├── error.diff.missing_new_line1-f │ ├── error.diff.missing_new_line2-e │ ├── error.diff.missing_new_line2-f │ ├── output.diff │ ├── output.diff-c │ ├── output.diff-e │ ├── output.diff-f │ ├── output.diff-u │ ├── output.diff.bin1 │ ├── output.diff.bin1-c │ ├── output.diff.bin1-e │ ├── output.diff.bin1-f │ ├── output.diff.bin1-u │ ├── output.diff.bin2 │ ├── output.diff.bin2-c │ ├── output.diff.bin2-e │ ├── output.diff.bin2-f │ ├── output.diff.bin2-u │ ├── output.diff.chef │ ├── output.diff.chef-c │ ├── output.diff.chef-e │ ├── output.diff.chef-f │ ├── output.diff.chef-u │ ├── output.diff.chef2 │ ├── output.diff.chef2-c │ ├── output.diff.chef2-d │ ├── output.diff.chef2-e │ ├── output.diff.chef2-f │ ├── output.diff.chef2-u │ ├── output.diff.empty.vs.four_lines │ ├── output.diff.empty.vs.four_lines-c │ ├── output.diff.empty.vs.four_lines-e │ ├── output.diff.empty.vs.four_lines-f │ ├── output.diff.empty.vs.four_lines-u │ ├── output.diff.four_lines.vs.empty │ ├── output.diff.four_lines.vs.empty-c │ ├── output.diff.four_lines.vs.empty-e │ ├── output.diff.four_lines.vs.empty-f │ ├── output.diff.four_lines.vs.empty-u │ ├── output.diff.issue95_trailing_context │ ├── output.diff.issue95_trailing_context-c │ ├── output.diff.issue95_trailing_context-e │ ├── output.diff.issue95_trailing_context-f │ ├── output.diff.issue95_trailing_context-u │ ├── output.diff.missing_new_line1 │ ├── output.diff.missing_new_line1-c │ ├── output.diff.missing_new_line1-e │ ├── output.diff.missing_new_line1-f │ ├── output.diff.missing_new_line1-u │ ├── output.diff.missing_new_line2 │ ├── output.diff.missing_new_line2-c │ ├── output.diff.missing_new_line2-e │ ├── output.diff.missing_new_line2-f │ └── output.diff.missing_new_line2-u ├── new-chef ├── new-chef2 ├── old-chef └── old-chef2 ├── hunk_spec.rb ├── issues_spec.rb ├── lcs_spec.rb ├── ldiff_spec.rb ├── patch_spec.rb ├── sdiff_spec.rb ├── spec_helper.rb ├── traverse_balanced_spec.rb └── traverse_sequences_spec.rb /.fasterer.yml: -------------------------------------------------------------------------------- 1 | exclude_paths: 2 | - research/**/* 3 | - pkg/**/* 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: halostatue 2 | buy_me_a_coffee: halostatue 3 | ko_fi: halostatue 4 | tidelift: rubygems/diff-lcs 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: weekly 8 | 9 | - package-ecosystem: bundler 10 | directory: / 11 | schedule: 12 | interval: monthly 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Ruby CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | lint: 14 | name: Lint 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Harden the runner 19 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 20 | with: 21 | disable-sudo: true 22 | egress-policy: block 23 | allowed-endpoints: > 24 | github.com:443 25 | index.rubygems.org:443 26 | objects.githubusercontent.com:443 27 | rubygems.org:443 28 | 29 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 30 | with: 31 | persist-credentials: false 32 | 33 | - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 34 | with: 35 | ruby-version: '3.4' 36 | rubygems: latest 37 | bundler: 2 38 | bundler-cache: false 39 | 40 | - run: | 41 | bundle install --jobs 4 --retry 3 42 | bundle exec standardrb 43 | env: 44 | MAINTENANCE: true 45 | 46 | coverage: 47 | name: Generate Coverage Report 48 | runs-on: ubuntu-latest 49 | 50 | steps: 51 | - name: Harden the runner 52 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 53 | with: 54 | disable-sudo: true 55 | egress-policy: block 56 | allowed-endpoints: > 57 | coveralls.io:443 58 | github.com:443 59 | index.rubygems.org:443 60 | objects.githubusercontent.com:443 61 | rubygems.org:443 62 | 63 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 64 | with: 65 | persist-credentials: false 66 | 67 | - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 68 | with: 69 | ruby-version: '3.4' 70 | rubygems: latest 71 | bundler: 2 72 | bundler-cache: false 73 | 74 | - run: | 75 | bundle install --jobs 4 --retry 3 76 | bundle exec ruby -S rake spec:coverage --trace 77 | env: 78 | COVERAGE: true 79 | 80 | - uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b #v2.3.6 81 | 82 | required-ubuntu: 83 | name: Ruby ${{ matrix.ruby }} - ${{ matrix.os }} 84 | 85 | strategy: 86 | fail-fast: false 87 | matrix: 88 | os: 89 | - ubuntu-22.04 90 | - ubuntu-24.04 91 | ruby: 92 | - '2.6' 93 | - '2.7' 94 | - '3.1' 95 | - '3.2' 96 | - '3.3' 97 | - '3.4' 98 | - truffleruby 99 | 100 | runs-on: ${{ matrix.os }} 101 | 102 | steps: 103 | - name: Harden the runner 104 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 105 | with: 106 | disable-sudo: true 107 | egress-policy: block 108 | allowed-endpoints: > 109 | github.com:443 110 | index.rubygems.org:443 111 | objects.githubusercontent.com:443 112 | rubygems.org:443 113 | 114 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 115 | with: 116 | persist-credentials: false 117 | 118 | - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 119 | with: 120 | ruby-version: ${{ matrix.ruby }} 121 | rubygems: latest 122 | bundler: 2 123 | bundler-cache: true 124 | 125 | - run: bundle exec ruby -S rake test --trace 126 | 127 | required-macos: 128 | name: Ruby ${{ matrix.ruby }} - ${{ matrix.os }} 129 | 130 | strategy: 131 | fail-fast: false 132 | matrix: 133 | os: 134 | - macos-13 135 | - macos-14 136 | - macos-15 137 | ruby: 138 | - '2.6' 139 | - '2.7' 140 | - '3.1' 141 | - '3.2' 142 | - '3.3' 143 | - '3.4' 144 | 145 | runs-on: ${{ matrix.os }} 146 | 147 | steps: 148 | - name: Harden the runner 149 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 150 | with: 151 | disable-sudo: true 152 | egress-policy: block 153 | allowed-endpoints: > 154 | github.com:443 155 | index.rubygems.org:443 156 | objects.githubusercontent.com:443 157 | rubygems.org:443 158 | 159 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 160 | with: 161 | persist-credentials: false 162 | 163 | - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 164 | with: 165 | ruby-version: ${{ matrix.ruby }} 166 | rubygems: latest 167 | bundler: 2 168 | bundler-cache: true 169 | 170 | - run: bundle exec ruby -S rake test --trace 171 | 172 | # Windows builds have become deeply unstable over the last several months. They no 173 | # longer require success to run. 174 | # 175 | # Windows 2019 / Ruby 3.0 does not include libyaml, so we are removing it from the test 176 | # cycle. All other versions succeed on Windows 2019 and Ruby 3.0 succeeds on Windows 177 | # 2022. https://github.com/ruby/setup-ruby/issues/641 178 | optional-windows: 179 | name: Ruby ${{ matrix.ruby }} - ${{ matrix.os }} 180 | 181 | strategy: 182 | fail-fast: false 183 | matrix: 184 | os: 185 | - windows-2019 186 | - windows-2022 187 | ruby: 188 | - '2.6' 189 | - '2.7' 190 | - '3.1' 191 | - '3.2' 192 | - '3.3' 193 | - '3.4' 194 | - mswin 195 | - ucrt 196 | include: 197 | - ruby: mingw 198 | os: windows-2022 199 | - ruby: '3.0' 200 | os: windows-2022 201 | 202 | continue-on-error: true 203 | runs-on: ${{ matrix.os }} 204 | 205 | steps: 206 | - name: Harden the runner 207 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 208 | with: 209 | disable-sudo: true 210 | egress-policy: block 211 | allowed-endpoints: > 212 | github.com:443 213 | index.rubygems.org:443 214 | objects.githubusercontent.com:443 215 | rubygems.org:443 216 | 217 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 218 | with: 219 | persist-credentials: false 220 | 221 | - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 222 | with: 223 | ruby-version: ${{ matrix.ruby }} 224 | rubygems: latest 225 | bundler: 2 226 | bundler-cache: true 227 | 228 | - run: bundle exec ruby -S rake test --trace 229 | 230 | ruby-head-optional: 231 | name: Ruby ${{ matrix.ruby }} - ${{ matrix.os }} (optional) 232 | 233 | strategy: 234 | fail-fast: false 235 | 236 | matrix: 237 | ruby: 238 | - head 239 | os: 240 | - macos-latest 241 | - ubuntu-latest 242 | - windows-latest 243 | 244 | continue-on-error: true 245 | runs-on: ${{ matrix.os }} 246 | 247 | steps: 248 | - name: Harden the runner 249 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 250 | with: 251 | disable-sudo: true 252 | egress-policy: block 253 | allowed-endpoints: > 254 | github.com:443 255 | index.rubygems.org:443 256 | objects.githubusercontent.com:443 257 | rubygems.org:443 258 | 259 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 260 | with: 261 | persist-credentials: false 262 | 263 | - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 264 | with: 265 | ruby-version: ${{ matrix.ruby }} 266 | rubygems: latest 267 | bundler: 2 268 | bundler-cache: true 269 | 270 | - run: bundle exec ruby -S rake test --trace 271 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [main] 4 | pull_request: 5 | # The branches below must be a subset of the branches above 6 | branches: [main] 7 | schedule: 8 | - cron: '28 4 * * 2' 9 | 10 | permissions: {} 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: ['ruby'] 25 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 26 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 31 | with: 32 | persist-credentials: false 33 | 34 | # Initializes the CodeQL tools for scanning. 35 | - name: Initialize CodeQL 36 | uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f #v3.28.18 37 | with: 38 | languages: ${{ matrix.language }} 39 | # If you wish to specify custom queries, you can do so here or in a config file. 40 | # By default, queries listed here will override any specified in a config file. 41 | # Prefix the list here with "+" to use these queries and those in the config file. 42 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 43 | 44 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 45 | # If this step fails, then you should remove it and run the build manually (see below) 46 | - name: Autobuild 47 | uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f #v3.28.18 48 | 49 | # ℹ️ Command-line programs to run using the OS shell. 50 | # 📚 https://git.io/JvXDl 51 | 52 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 53 | # and modify them (or add more) to build your code if your project 54 | # uses a compiled language 55 | 56 | #- run: | 57 | # make bootstrap 58 | # make release 59 | 60 | - name: Perform CodeQL Analysis 61 | uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f #v3.28.18 62 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, 4 | # surfacing known-vulnerable versions of the packages declared or updated in the PR. Once 5 | # installed, if the workflow run is marked as required, PRs introducing known-vulnerable 6 | # packages will be blocked from merging. 7 | # 8 | # Source repository: https://github.com/actions/dependency-review-action 9 | name: 'Dependency Review' 10 | on: [pull_request] 11 | 12 | permissions: {} 13 | 14 | jobs: 15 | dependency-review: 16 | permissions: 17 | contents: read 18 | 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Harden the runner 22 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 23 | with: 24 | disable-sudo: true 25 | egress-policy: block 26 | allowed-endpoints: > 27 | api.github.com:443 28 | api.securityscorecards.dev:443 29 | github.com:443 30 | 31 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 32 | with: 33 | persist-credentials: false 34 | 35 | - name: 'Dependency Review' 36 | uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 37 | -------------------------------------------------------------------------------- /.github/workflows/publish-gem.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - lib/diff/lcs/version.rb 9 | 10 | pull_request: 11 | branches: 12 | - main 13 | types: 14 | - closed 15 | paths: 16 | - lib/diff/lcs/version.rb 17 | 18 | workflow_dispatch: 19 | 20 | permissions: {} 21 | 22 | jobs: 23 | release: 24 | if: github.repository == 'halostatue/diff-lcs' && (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged)) 25 | 26 | runs-on: ubuntu-latest 27 | environment: release 28 | 29 | env: 30 | rubygems_release_gem: true 31 | 32 | permissions: 33 | contents: write 34 | id-token: write 35 | 36 | steps: 37 | - name: Harden the runner 38 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 39 | with: 40 | disable-sudo: true 41 | egress-policy: block 42 | allowed-endpoints: > 43 | fulcio.sigstore.dev:443 44 | github.com:443 45 | index.rubygems.org:443 46 | objects.githubusercontent.com:443 47 | rekor.sigstore.dev:443 48 | rubygems.org:443 49 | tuf-repo-cdn.sigstore.dev:443 50 | 51 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 52 | with: 53 | persist-credentials: true 54 | 55 | - id: rubygems 56 | run: | 57 | ruby -e \ 58 | 'print "version=", Gem::Specification.load(ARGV[0]).rubygems_version, "\n"' \ 59 | diff-lcs.gemspec >>"${GITHUB_OUTPUT}" 60 | 61 | - uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 62 | with: 63 | bundler-cache: false 64 | ruby-version: ruby 65 | 66 | - name: Install dependencies 67 | run: | 68 | gem update --system="${RUBYGEMS_VERSION}" 69 | bundle install --jobs 4 --retry 3 70 | env: 71 | RUBYGEMS_VERSION: ${{ steps.rubygems.outputs.version }} 72 | 73 | - name: Get gem version 74 | run: | 75 | echo gem_version="$(rake version)" >>"${GITHUB_OUTPUT}" 76 | 77 | - name: Ensure that gemspec is up-to-date 78 | run: | 79 | rake gemspec 80 | 81 | if ! git diff >/dev/null 2>/dev/null; then 82 | git commit -m "chore: Automatically update gemspec for ${gem_version} for release" 83 | git push origin main 84 | fi 85 | 86 | - uses: rubygems/release-gem@a25424ba2ba8b387abc8ef40807c2c85b96cbe32 # v1.1.1 87 | 88 | - name: Update documentation 89 | run: | 90 | rake docs 91 | git checkout gh-pages 92 | 93 | find . -mindepth 1 -maxdepth 1 \ 94 | \( ! -name '.git' -a ! -name '.gitignore' -a ! -name 'doc' \) \ 95 | -exec rm -rf {} + 96 | 97 | cp -r doc/* . 98 | git add . 99 | git commit -m "Update documentation for ${gem_version}" 100 | git push origin gh-pages 101 | -------------------------------------------------------------------------------- /.github/workflows/reviewdog.yml: -------------------------------------------------------------------------------- 1 | name: Reviewdog 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: {} 7 | 8 | jobs: 9 | typos: 10 | if: ${{ github.event.action != 'closed' }} 11 | name: Typos 12 | runs-on: ubuntu-22.04 13 | 14 | permissions: 15 | contents: read 16 | pull-requests: write 17 | 18 | steps: 19 | - name: Harden Runner 20 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 21 | with: 22 | disable-sudo: true 23 | egress-policy: block 24 | allowed-endpoints: > 25 | api.github.com:443 26 | github.com:443 27 | objects.githubusercontent.com:443 28 | raw.githubusercontent.com:443 29 | 30 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 31 | with: 32 | persist-credentials: false 33 | 34 | - uses: reviewdog/action-typos@627388e238f182b925d9acd151432f9b68f1d666 # v1.17.2 35 | 36 | actionlint: 37 | if: ${{ github.event.action != 'closed' }} 38 | name: Actionlint 39 | runs-on: ubuntu-22.04 40 | 41 | permissions: 42 | contents: read 43 | pull-requests: write 44 | 45 | steps: 46 | - name: Harden Runner 47 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 48 | with: 49 | disable-sudo: true 50 | egress-policy: block 51 | allowed-endpoints: > 52 | api.github.com:443 53 | github.com:443 54 | 55 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 56 | with: 57 | persist-credentials: false 58 | 59 | - uses: reviewdog/action-actionlint@a5524e1c19e62881d79c1f1b9b6f09f16356e281 # v1.65.2 60 | 61 | # standardrb: 62 | # if: ${{ github.event.action != 'closed' }} 63 | # name: 'Ruby: Standard' 64 | # runs-on: ubuntu-22.04 65 | 66 | # permissions: 67 | # contents: read 68 | # pull-requests: write 69 | 70 | # steps: 71 | # - name: Harden Runner 72 | # uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 73 | # with: 74 | # disable-sudo: true 75 | # egress-policy: block 76 | # allowed-endpoints: > 77 | # api.github.com:443 78 | # github.com:443 79 | # index.rubygems.org:443 80 | # objects.githubusercontent.com:443 81 | # raw.githubusercontent.com:443 82 | # rubygems.org:443 83 | 84 | # - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 85 | # with: 86 | # persist-credentials: false 87 | 88 | # - uses: ruby/setup-ruby@1a615958ad9d422dd932dc1d5823942ee002799f # v1.227.0 89 | # with: 90 | # ruby-version: '3.4' 91 | # bundler-cache: true 92 | 93 | # - uses: kirillplatonov/action-standard@ce7fc0be158421b01e5d9dc20eef1dcabcf18e4b # v1.0.1 94 | # with: 95 | # skip_install: true 96 | # use_bundler: true 97 | # env: 98 | # MAINTENANCE: true 99 | -------------------------------------------------------------------------------- /.github/workflows/scorecards.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided by 2 | # a third-party and are governed by separate terms of service, privacy policy, and support 3 | # documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '20 7 * * 2' 14 | push: 15 | branches: ['main'] 16 | 17 | # Declare default permissions as read only. 18 | permissions: {} 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | contents: read 30 | actions: read 31 | # To allow GraphQL ListCommits to work 32 | issues: read 33 | pull-requests: read 34 | # To detect SAST tools 35 | checks: read 36 | 37 | steps: 38 | - name: Harden the runner 39 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 40 | with: 41 | disable-sudo: true 42 | egress-policy: block 43 | allowed-endpoints: > 44 | api.deps.dev:443 45 | api.github.com:443 46 | api.osv.dev:443 47 | api.scorecard.dev:443 48 | fulcio.sigstore.dev:443 49 | github.com:443 50 | oss-fuzz-build-logs.storage.googleapis.com:443 51 | rekor.sigstore.dev:443 52 | tuf-repo-cdn.sigstore.dev:443 53 | www.bestpractices.dev:443 54 | 55 | - name: "Checkout code" 56 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 57 | with: 58 | persist-credentials: false 59 | 60 | - name: "Run analysis" 61 | uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 62 | with: 63 | results_file: results.sarif 64 | results_format: sarif 65 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 66 | # 67 | # - you want to enable the Branch-Protection check on a *public* repository, or 68 | # - you are installing Scorecards on a *private* repository 69 | # 70 | # To create the PAT, follow the steps in 71 | # https://github.com/ossf/scorecard-action#authentication-with-pat. 72 | # 73 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 74 | 75 | # Public repositories: 76 | # - Publish results to OpenSSF REST API for easy access by consumers 77 | # - Allows the repository to include the Scorecard badge. 78 | # - See https://github.com/ossf/scorecard-action#publishing-results. 79 | # 80 | # For private repositories: 81 | # - `publish_results` will always be set to `false`, regardless 82 | # of the value entered here. 83 | publish_results: true 84 | 85 | # Upload the results as artifacts (optional). Commenting out will disable uploads of 86 | # run results in SARIF format to the repository Actions tab. 87 | - name: "Upload artifact" 88 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 89 | with: 90 | name: SARIF file 91 | path: results.sarif 92 | retention-days: 5 93 | 94 | # Upload the results to GitHub's code scanning dashboard. 95 | - name: "Upload to code-scanning" 96 | uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 97 | with: 98 | sarif_file: results.sarif 99 | -------------------------------------------------------------------------------- /.github/workflows/zizmor.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Security Analysis with zizmor 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | zizmor: 12 | name: zizmor latest via uv 13 | runs-on: ubuntu-latest 14 | 15 | permissions: 16 | security-events: write 17 | # required for workflows in private repositories 18 | contents: read 19 | actions: read 20 | 21 | steps: 22 | - name: Harden Runner 23 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 24 | with: 25 | disable-sudo: true 26 | egress-policy: block 27 | allowed-endpoints: > 28 | api.github.com:443 29 | files.pythonhosted.org:443 30 | github.com:443 31 | objects.githubusercontent.com:443 32 | pypi.org:443 33 | 34 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 35 | with: 36 | persist-credentials: false 37 | 38 | - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 39 | 40 | - run: uvx zizmor --persona pedantic --format sarif . > results.sarif 41 | env: 42 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | 44 | - uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 45 | with: 46 | sarif_file: results.sarif 47 | category: zizmor 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle/ 3 | .byebug_history 4 | .rake_tasks~ 5 | .source_index 6 | /benchmarks/ 7 | /coverage/ 8 | /doc/ 9 | /html/ 10 | /pkg/ 11 | /publish/ 12 | /research/ 13 | /test/cache.tst 14 | /tmp/ 15 | /vendor/ 16 | /website/index.html 17 | Gemfile.lock 18 | mise.toml 19 | -------------------------------------------------------------------------------- /.hoerc: -------------------------------------------------------------------------------- 1 | --- 2 | exclude: !ruby/regexp '/ 3 | (?i:TAGS)$ 4 | | [Aa]ppraisals$ 5 | | [gG]emfile(?:\.lock)?$ 6 | | \.gemspec$ 7 | | \.swp$ 8 | | \.tmp$ 9 | | ^\.appveyor\.ya?ml$ 10 | | ^\.autotest$ 11 | | ^\.bundle\/ 12 | | ^\.byebug_history$ 13 | | ^\.coveralls\.ya?ml$ 14 | | ^\.DS_Store$ 15 | | ^\.fasterer\.ya?ml$ 16 | | ^\.gemtest$ 17 | | ^\.git\/ 18 | | ^\.gitattributes$ 19 | | ^\.github\/ 20 | | ^\.gitignore$ 21 | | ^\.hg\/ 22 | | ^\.hoerc$ 23 | | ^\.idea\/ 24 | | ^\.pullreview\.ya?ml$ 25 | | ^\.rubocop.*\.ya?ml$ 26 | | ^\.standard.*\.ya?ml$ 27 | | ^\.svn\/ 28 | | ^\.travis\.ya?ml$ 29 | | ^\.typos\.toml$ 30 | | ^\.unused\.ya?ml$ 31 | | ^benchmarks\/ 32 | | ^coverage\/ 33 | | ^doc\/ 34 | | ^research\/ 35 | | ^support\/ 36 | | ^vendor\/ 37 | /x' 38 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | --- 2 | parallel: true 3 | ruby_version: 2.0 4 | 5 | ignore: 6 | - '*.gemspec' 7 | - research/**/* 8 | - pkg/**/* 9 | - Gemfile: 10 | - Style/HashSyntax 11 | - Rakefile: 12 | - Layout/HeredocIndentation 13 | 14 | plugins: 15 | - rubocop-thread_safety 16 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [default] 2 | extend-ignore-re = ["Ned Konz"] 3 | 4 | [default.extend-identifiers] 5 | 6 | [default.extend-words] 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official email address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at [INSERT CONTACT 63 | METHOD]. All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.1, available at 118 | . 119 | 120 | Community Impact Guidelines were inspired by 121 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 122 | 123 | For answers to common questions about this code of conduct, see the FAQ at 124 | . Translations are available at 125 | . 126 | 127 | [homepage]: https://www.contributor-covenant.org 128 | [Mozilla CoC]: https://github.com/mozilla/diversity 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contribution to diff-lcs is encouraged in any form: a bug report, a feature 4 | request, or code contributions. There are a few DOs and DON'Ts for 5 | contributions. 6 | 7 | - DO: 8 | 9 | - Keep the coding style that already exists for any updated Ruby code (support 10 | or otherwise). I use [Standard Ruby][standardrb] for linting and formatting. 11 | 12 | - Use thoughtfully-named topic branches for contributions. Rebase your commits 13 | into logical chunks as necessary. 14 | 15 | - Use [quality commit messages][qcm]. 16 | 17 | - Add your name or GitHub handle to `CONTRIBUTORS.md` and a record in the 18 | `CHANGELOG.md` as a separate commit from your main change. (Follow the style 19 | in the `CHANGELOG.md` and provide a link to your PR.) 20 | 21 | - Add or update tests as appropriate for your change. The test suite is 22 | written in [RSpec][rspec]. 23 | 24 | - Add or update documentation as appropriate for your change. The 25 | documentation is RDoc; diff-lcs does not use extensions that may be present 26 | in alternative documentation generators. 27 | 28 | - DO NOT: 29 | 30 | - Modify `VERSION` in `lib/diff/lcs/version.rb`. When your patch is accepted 31 | and a release is made, the version will be updated at that point. 32 | 33 | - Modify `diff-lcs.gemspec`; it is a generated file. (You _may_ use 34 | `rake gemspec` to regenerate it if your change involves metadata related to 35 | gem itself). 36 | 37 | - Modify the `Gemfile`. 38 | 39 | ## Test Dependencies 40 | 41 | diff-lcs uses Ryan Davis's [Hoe][Hoe] to manage the release process, and it adds 42 | a number of rake tasks. You will mostly be interested in `rake`, which runs 43 | tests in the same way that `rake spec` does. 44 | 45 | To assist with the installation of the development dependencies for diff-lcs, I 46 | have provided a Gemfile pointing to the (generated) `diff-lcs.gemspec` file. 47 | `minitar.gemspec` file. This will permit you to use `bundle install` to install 48 | the dependencies. 49 | 50 | You can run tests with code coverage analysis by running `rake spec:coverage`. 51 | 52 | ## Workflow 53 | 54 | Here's the most direct way to get your work merged into the project: 55 | 56 | - Fork the project. 57 | - Clone your fork (`git clone git://github.com//diff-lcs.git`). 58 | - Create a topic branch to contain your change 59 | (`git checkout -b my_awesome_feature`). 60 | - Hack away, add tests. Not necessarily in that order. 61 | - Make sure everything still passes by running `rake`. 62 | - If necessary, rebase your commits into logical chunks, without errors. 63 | - Push the branch up (`git push origin my_awesome_feature`). 64 | - Create a pull request against halostatue/diff-lcs and describe what your 65 | change does and the why you think it should be merged. 66 | 67 | [hoe]: https://github.com/seattlerb/hoe 68 | [qcm]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 69 | [release-gem]: https://github.com/rubygems/release-gem 70 | [rspec]: http://rspec.info/documentation/ 71 | [standardrb]: https://github.com/standardrb/standard 72 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | - Austin Ziegler (@halostatue) created diff-lcs. 4 | 5 | Thanks to everyone else who has contributed to diff-lcs over the years: 6 | 7 | - @ginriki 8 | - @joshbronson 9 | - @kevinmook 10 | - @mckaz 11 | - Akinori Musha 12 | - Artem Ignatyev 13 | - Brandon Fish 14 | - Baptiste Courtois (@annih) 15 | - Camille Drapier 16 | - Cédric Boutillier 17 | - @earlopain 18 | - Gregg Kellogg 19 | - Jagdeep Singh 20 | - Jason Gladish 21 | - Jon Rowe 22 | - Josef Strzibny 23 | - Josep (@apuratepp) 24 | - Josh Bronson 25 | - Jun Aruga 26 | - Justin Steele 27 | - Kenichi Kamiya 28 | - Kensuke Nagae 29 | - Kevin Ansfield 30 | - Koichi Ito 31 | - Mark Friedgan 32 | - Masato Nakamura 33 | - Mark Young 34 | - Michael Granger 35 | - Myron Marston 36 | - Nicolas Leger 37 | - Oleg Orlov 38 | - Patrick Linnane 39 | - Paul Kunysch 40 | - Pete Higgins 41 | - Peter Goldstein 42 | - Peter Wagenet 43 | - Philippe Lafoucrière 44 | - Ryan Lovelett 45 | - Scott Steele 46 | - Simon Courtois 47 | - Tien (@tiendo1011) 48 | - Tomas Jura 49 | - Vít Ondruch 50 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # NOTE: This file is not the canonical source of dependencies. Edit the 2 | # Rakefile, instead. 3 | 4 | source "https://rubygems.org/" 5 | 6 | if ENV["DEV"] 7 | gem "debug", :platforms => [:mri] 8 | end 9 | 10 | if ENV["COVERAGE"] 11 | gem "simplecov", :require => false, :platforms => [:mri_34] 12 | gem "simplecov-lcov", :require => false, :platforms => [:mri_34] 13 | end 14 | 15 | if ENV["MAINTENANCE"] 16 | gem "standard", :require => false, :platforms => [:mri_34] 17 | gem "standard-thread_safety", :require => false, :platforms => [:mri_34] 18 | gem "fasterer", :require => false, :platforms => [:mri_34] 19 | end 20 | 21 | gemspec 22 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | # Licence 2 | 3 | This software is available under three licenses: the GNU GPL version 2 (or at 4 | your option, a later version), the Perl Artistic license, or the MIT license. 5 | Note that my preference for licensing is the MIT license, but Algorithm::Diff 6 | was dually originally licensed with the Perl Artistic and the GNU GPL ("the same 7 | terms as Perl itself") and given that the Ruby implementation originally hewed 8 | pretty closely to the Perl version, I must maintain the additional licensing 9 | terms. 10 | 11 | - Copyright 2004–2025 Austin Ziegler and contributors. 12 | - Adapted from Algorithm::Diff (Perl) by Ned Konz and a Smalltalk version by 13 | Mario I. Wolczko. 14 | 15 | ## MIT License 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining a copy of 18 | this software and associated documentation files (the "Software"), to deal in 19 | the Software without restriction, including without limitation the rights to 20 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 21 | the Software, and to permit persons to whom the Software is furnished to do so, 22 | subject to the following conditions: 23 | 24 | The above copyright notice and this permission notice shall be included in all 25 | copies or substantial portions of the Software. 26 | 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 29 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 30 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 31 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 32 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 33 | 34 | ## Perl Artistic License 35 | 36 | See the file docs/artistic.txt in the main distribution. 37 | 38 | ## GNU GPL version 2 39 | 40 | See the file docs/COPYING.txt in the main distribution. 41 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | .rspec 2 | CHANGELOG.md 3 | CODE_OF_CONDUCT.md 4 | CONTRIBUTING.md 5 | CONTRIBUTORS.md 6 | LICENCE.md 7 | Manifest.txt 8 | README.md 9 | Rakefile 10 | SECURITY.md 11 | bin/htmldiff 12 | bin/ldiff 13 | docs/COPYING.txt 14 | docs/artistic.txt 15 | lib/diff-lcs.rb 16 | lib/diff/lcs.rb 17 | lib/diff/lcs/array.rb 18 | lib/diff/lcs/backports.rb 19 | lib/diff/lcs/block.rb 20 | lib/diff/lcs/callbacks.rb 21 | lib/diff/lcs/change.rb 22 | lib/diff/lcs/htmldiff.rb 23 | lib/diff/lcs/hunk.rb 24 | lib/diff/lcs/internals.rb 25 | lib/diff/lcs/ldiff.rb 26 | lib/diff/lcs/string.rb 27 | lib/diff/lcs/version.rb 28 | mise.toml 29 | spec/change_spec.rb 30 | spec/diff_spec.rb 31 | spec/fixtures/123_x 32 | spec/fixtures/456_x 33 | spec/fixtures/aX 34 | spec/fixtures/bXaX 35 | spec/fixtures/ds1.csv 36 | spec/fixtures/ds2.csv 37 | spec/fixtures/empty 38 | spec/fixtures/file1.bin 39 | spec/fixtures/file2.bin 40 | spec/fixtures/four_lines 41 | spec/fixtures/four_lines_with_missing_new_line 42 | spec/fixtures/ldiff/diff.missing_new_line1-e 43 | spec/fixtures/ldiff/diff.missing_new_line1-f 44 | spec/fixtures/ldiff/diff.missing_new_line2-e 45 | spec/fixtures/ldiff/diff.missing_new_line2-f 46 | spec/fixtures/ldiff/error.diff.chef-e 47 | spec/fixtures/ldiff/error.diff.chef-f 48 | spec/fixtures/ldiff/error.diff.missing_new_line1-e 49 | spec/fixtures/ldiff/error.diff.missing_new_line1-f 50 | spec/fixtures/ldiff/error.diff.missing_new_line2-e 51 | spec/fixtures/ldiff/error.diff.missing_new_line2-f 52 | spec/fixtures/ldiff/output.diff 53 | spec/fixtures/ldiff/output.diff-c 54 | spec/fixtures/ldiff/output.diff-e 55 | spec/fixtures/ldiff/output.diff-f 56 | spec/fixtures/ldiff/output.diff-u 57 | spec/fixtures/ldiff/output.diff.bin1 58 | spec/fixtures/ldiff/output.diff.bin1-c 59 | spec/fixtures/ldiff/output.diff.bin1-e 60 | spec/fixtures/ldiff/output.diff.bin1-f 61 | spec/fixtures/ldiff/output.diff.bin1-u 62 | spec/fixtures/ldiff/output.diff.bin2 63 | spec/fixtures/ldiff/output.diff.bin2-c 64 | spec/fixtures/ldiff/output.diff.bin2-e 65 | spec/fixtures/ldiff/output.diff.bin2-f 66 | spec/fixtures/ldiff/output.diff.bin2-u 67 | spec/fixtures/ldiff/output.diff.chef 68 | spec/fixtures/ldiff/output.diff.chef-c 69 | spec/fixtures/ldiff/output.diff.chef-e 70 | spec/fixtures/ldiff/output.diff.chef-f 71 | spec/fixtures/ldiff/output.diff.chef-u 72 | spec/fixtures/ldiff/output.diff.chef2 73 | spec/fixtures/ldiff/output.diff.chef2-c 74 | spec/fixtures/ldiff/output.diff.chef2-d 75 | spec/fixtures/ldiff/output.diff.chef2-e 76 | spec/fixtures/ldiff/output.diff.chef2-f 77 | spec/fixtures/ldiff/output.diff.chef2-u 78 | spec/fixtures/ldiff/output.diff.empty.vs.four_lines 79 | spec/fixtures/ldiff/output.diff.empty.vs.four_lines-c 80 | spec/fixtures/ldiff/output.diff.empty.vs.four_lines-e 81 | spec/fixtures/ldiff/output.diff.empty.vs.four_lines-f 82 | spec/fixtures/ldiff/output.diff.empty.vs.four_lines-u 83 | spec/fixtures/ldiff/output.diff.four_lines.vs.empty 84 | spec/fixtures/ldiff/output.diff.four_lines.vs.empty-c 85 | spec/fixtures/ldiff/output.diff.four_lines.vs.empty-e 86 | spec/fixtures/ldiff/output.diff.four_lines.vs.empty-f 87 | spec/fixtures/ldiff/output.diff.four_lines.vs.empty-u 88 | spec/fixtures/ldiff/output.diff.issue95_trailing_context 89 | spec/fixtures/ldiff/output.diff.issue95_trailing_context-c 90 | spec/fixtures/ldiff/output.diff.issue95_trailing_context-e 91 | spec/fixtures/ldiff/output.diff.issue95_trailing_context-f 92 | spec/fixtures/ldiff/output.diff.issue95_trailing_context-u 93 | spec/fixtures/ldiff/output.diff.missing_new_line1 94 | spec/fixtures/ldiff/output.diff.missing_new_line1-c 95 | spec/fixtures/ldiff/output.diff.missing_new_line1-e 96 | spec/fixtures/ldiff/output.diff.missing_new_line1-f 97 | spec/fixtures/ldiff/output.diff.missing_new_line1-u 98 | spec/fixtures/ldiff/output.diff.missing_new_line2 99 | spec/fixtures/ldiff/output.diff.missing_new_line2-c 100 | spec/fixtures/ldiff/output.diff.missing_new_line2-e 101 | spec/fixtures/ldiff/output.diff.missing_new_line2-f 102 | spec/fixtures/ldiff/output.diff.missing_new_line2-u 103 | spec/fixtures/new-chef 104 | spec/fixtures/new-chef2 105 | spec/fixtures/old-chef 106 | spec/fixtures/old-chef2 107 | spec/hunk_spec.rb 108 | spec/issues_spec.rb 109 | spec/lcs_spec.rb 110 | spec/ldiff_spec.rb 111 | spec/patch_spec.rb 112 | spec/sdiff_spec.rb 113 | spec/spec_helper.rb 114 | spec/traverse_balanced_spec.rb 115 | spec/traverse_sequences_spec.rb 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Diff::LCS 2 | 3 | - home :: https://github.com/halostatue/diff-lcs 4 | - changelog :: https://github.com/halostatue/diff-lcs/blob/main/CHANGELOG.md 5 | - code :: https://github.com/halostatue/diff-lcs 6 | - bugs :: https://github.com/halostatue/diff-lcs/issues 7 | - rdoc :: http://rubydoc.info/github/halostatue/diff-lcs 8 | 9 | 10 | 11 | 12 | 13 | ## Description 14 | 15 | Diff::LCS computes the difference between two Enumerable sequences using the 16 | McIlroy-Hunt longest common subsequence (LCS) algorithm. It includes utilities 17 | to create a simple HTML diff output format and a standard diff-like tool. 18 | 19 | This is release 1.6.1, providing a simple extension that allows for 20 | Diff::LCS::Change objects to be treated implicitly as arrays and fixes a number 21 | of formatting issues. 22 | 23 | Ruby versions below 2.5 are soft-deprecated, which means that older versions are 24 | no longer part of the CI test suite. If any changes have been introduced that 25 | break those versions, bug reports and patches will be accepted, but it will be 26 | up to the reporter to verify any fixes prior to release. The next major release 27 | will completely break compatibility. 28 | 29 | ## Synopsis 30 | 31 | Using this module is quite simple. By default, Diff::LCS does not extend objects 32 | with the Diff::LCS interface, but will be called as if it were a function: 33 | 34 | ```ruby 35 | require 'diff/lcs' 36 | 37 | seq1 = %w(a b c e h j l m n p) 38 | seq2 = %w(b c d e f j k l m r s t) 39 | 40 | lcs = Diff::LCS.LCS(seq1, seq2) 41 | diffs = Diff::LCS.diff(seq1, seq2) 42 | sdiff = Diff::LCS.sdiff(seq1, seq2) 43 | seq = Diff::LCS.traverse_sequences(seq1, seq2, callback_obj) 44 | bal = Diff::LCS.traverse_balanced(seq1, seq2, callback_obj) 45 | seq2 == Diff::LCS.patch!(seq1, diffs) 46 | seq1 == Diff::LCS.unpatch!(seq2, diffs) 47 | seq2 == Diff::LCS.patch!(seq1, sdiff) 48 | seq1 == Diff::LCS.unpatch!(seq2, sdiff) 49 | ``` 50 | 51 | Objects can be extended with Diff::LCS: 52 | 53 | ```ruby 54 | seq1.extend(Diff::LCS) 55 | lcs = seq1.lcs(seq2) 56 | diffs = seq1.diff(seq2) 57 | sdiff = seq1.sdiff(seq2) 58 | seq = seq1.traverse_sequences(seq2, callback_obj) 59 | bal = seq1.traverse_balanced(seq2, callback_obj) 60 | seq2 == seq1.patch!(diffs) 61 | seq1 == seq2.unpatch!(diffs) 62 | seq2 == seq1.patch!(sdiff) 63 | seq1 == seq2.unpatch!(sdiff) 64 | ``` 65 | 66 | By requiring 'diff/lcs/array' or 'diff/lcs/string', Array or String will be 67 | extended for use this way. 68 | 69 | Note that Diff::LCS requires a sequenced enumerable container, which means that 70 | the order of enumeration is both predictable and consistent for the same set of 71 | data. While it is theoretically possible to generate a diff for an unordered 72 | hash, it will only be meaningful if the enumeration of the hashes is consistent. 73 | In general, this will mean that containers that behave like String or Array will 74 | perform best. 75 | 76 | ## History 77 | 78 | Diff::LCS is a port of Perl's Algorithm::Diff that uses the McIlroy-Hunt longest 79 | common subsequence (LCS) algorithm to compute intelligent differences between 80 | two sequenced enumerable containers. The implementation is based on Mario I. 81 | Wolczko's [Smalltalk version 1.2][smalltalk] (1993) and Ned Konz's Perl version 82 | [Algorithm::Diff 1.15][perl]. `Diff::LCS#sdiff` and 83 | `Diff::LCS#traverse_balanced` were originally written for the Perl version by 84 | Mike Schilli. 85 | 86 | The algorithm is described in A Fast Algorithm for Computing Longest Common 87 | Subsequences, CACM, vol.20, no.5, pp.350-353, May 1977, with a few minor 88 | improvements to improve the speed. A simplified description of the algorithm, 89 | originally written for the Perl version, was written by Mark-Jason Dominus. 90 | 91 | [smalltalk]: ftp://st.cs.uiuc.edu/pub/Smalltalk/MANCHESTER/manchester/4.0/diff.st 92 | [perl]: http://search.cpan.org/~nedkonz/Algorithm-Diff-1.15/ 93 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "rspec" 3 | require "rspec/core/rake_task" 4 | require "hoe" 5 | require "rake/clean" 6 | 7 | MAINTENANCE = ENV["MAINTENANCE"] == "true" 8 | BUILD_DOCS = MAINTENANCE || ENV["DOCS"] == "true" 9 | TRUSTED_RELEASE = ENV["rubygems_release_gem"] == "true" 10 | 11 | Hoe.plugin :halostatue 12 | Hoe.plugin :rubygems 13 | 14 | Hoe.plugins.delete :debug 15 | Hoe.plugins.delete :newb 16 | Hoe.plugins.delete :signing 17 | Hoe.plugins.delete :publish unless BUILD_DOCS 18 | 19 | if RUBY_VERSION < "1.9" 20 | class Array # :nodoc: 21 | def to_h 22 | Hash[*flatten(1)] 23 | end 24 | end 25 | 26 | class Gem::Specification # :nodoc: 27 | def metadata=(*) 28 | end 29 | 30 | def default_value(*) 31 | end 32 | end 33 | 34 | class Object # :nodoc: 35 | def caller_locations(*) 36 | [] 37 | end 38 | end 39 | end 40 | 41 | _spec = Hoe.spec "diff-lcs" do 42 | developer("Austin Ziegler", "halostatue@gmail.com") 43 | 44 | self.trusted_release = TRUSTED_RELEASE 45 | 46 | require_ruby_version ">= 1.8" 47 | 48 | self.history_file = "CHANGELOG.md" 49 | self.readme_file = "README.md" 50 | self.licenses = ["MIT", "Artistic-1.0-Perl", "GPL-2.0-or-later"] 51 | 52 | spec_extras[:metadata] = ->(val) { 53 | val["rubygems_mfa_required"] = "true" 54 | } 55 | 56 | extra_dev_deps << ["hoe", "~> 4.0"] 57 | extra_dev_deps << ["hoe-halostatue", "~> 2.0"] 58 | extra_dev_deps << ["hoe-rubygems", "~> 1.0"] 59 | extra_dev_deps << ["rspec", ">= 2.0", "< 4"] 60 | extra_dev_deps << ["rake", ">= 10.0", "< 14"] 61 | extra_dev_deps << ["rdoc", ">= 6.3.1", "< 7"] 62 | end 63 | 64 | if BUILD_DOCS 65 | rake_tasks = Rake.application.instance_variable_get(:@tasks) 66 | tasks = ["publish_docs", "publish_on_announce", "debug_email", "post_blog", "announce"] 67 | tasks.each do |task| 68 | rake_tasks.delete(task) 69 | end 70 | end 71 | 72 | desc "Run all specifications" 73 | RSpec::Core::RakeTask.new(:spec) do |t| 74 | rspec_dirs = %w[spec lib].join(":") 75 | t.rspec_opts = ["-I#{rspec_dirs}"] 76 | end 77 | 78 | task :version do 79 | require "diff/lcs/version" 80 | puts Diff::LCS::VERSION 81 | end 82 | 83 | Rake::Task["spec"].actions.uniq! { |a| a.source_location } 84 | 85 | # standard:disable Style/HashSyntax 86 | task :default => :spec unless Rake::Task["default"].prereqs.include?("spec") 87 | task :test => :spec unless Rake::Task["test"].prereqs.include?("spec") 88 | # standard:enable Style/HashSyntax 89 | 90 | if RUBY_VERSION >= "3.0" && RUBY_ENGINE == "ruby" 91 | namespace :spec do 92 | desc "Runs test coverage. Only works Ruby 2.0+ and assumes 'simplecov' is installed." 93 | task :coverage do 94 | ENV["COVERAGE"] = "true" 95 | Rake::Task["spec"].execute 96 | end 97 | end 98 | end 99 | 100 | if MAINTENANCE 101 | task ruby18: :package do 102 | require "diff/lcs/version" 103 | # standard:disable Layout/HeredocIndentation 104 | puts <<-MESSAGE 105 | You are starting a barebones Ruby 1.8 docker environment for testing. 106 | A snapshot package has been built, so install it with: 107 | 108 | cd diff-lcs 109 | gem install pkg/diff-lcs-#{Diff::LCS::VERSION} 110 | 111 | MESSAGE 112 | # standard:enable Layout/HeredocIndentation 113 | sh "docker run -it --rm -v #{Dir.pwd}:/root/diff-lcs bellbind/docker-ruby18-rails2 bash -l" 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # diff-lcs Security 2 | 3 | ## Supported Versions 4 | 5 | Security reports are accepted for the most recent major release and the previous 6 | version for a limited time after the initial major release version. After a 7 | major release, the previous version will receive full support for six months and 8 | security support for an additional six months (for a total of twelve months). 9 | 10 | Because diff-lcs 1.x supports a wide range of Ruby versions, security reports 11 | will only be accepted when they can be demonstrated on Ruby 3.1 or higher. 12 | 13 | > [!information] 14 | > 15 | > There will be a diff-lcs 2.0 released in 2025 which narrows support to modern 16 | > versions of Ruby only. 17 | > 18 | > | Release Date | Support Ends | Security Support Ends | 19 | > | ------------ | ------------ | --------------------- | 20 | > | 2025 | +6 months | +12 months | 21 | > 22 | > If the 2.0.0 release happens on 2025-07-01, regular support for diff-lcs 1.x 23 | > will end on 2026-12-31 and security support for diff-lcs 1.x will end on 24 | > 2026-06-30. 25 | 26 | ## Reporting a Vulnerability 27 | 28 | By preference, use the [Tidelift security contact][tidelift]. Tidelift will 29 | coordinate the fix and disclosure. 30 | 31 | Alternatively, Send an email to [diff-lcs@halostatue.ca][email] with the text 32 | `Diff::LCS` in the subject. Emails sent to this address should be encrypted 33 | using [age][age] with the following public key: 34 | 35 | ``` 36 | age1fc6ngxmn02m62fej5cl30lrvwmxn4k3q2atqu53aatekmnqfwumqj4g93w 37 | ``` 38 | 39 | [tidelift]: https://tidelift.com/security 40 | [email]: mailto:diff-lcs@halostatue.ca 41 | [age]: https://github.com/FiloSottile/age 42 | -------------------------------------------------------------------------------- /bin/htmldiff: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby -w 2 | # frozen_string_literal: true 3 | 4 | require "diff/lcs" 5 | require "diff/lcs/htmldiff" 6 | 7 | begin 8 | require "text/format" 9 | rescue LoadError 10 | Diff::LCS::HTMLDiff.can_expand_tabs = false 11 | end 12 | 13 | if ARGV.size < 2 or ARGV.size > 3 14 | warn "usage: #{File.basename($0)} old new [output.html]" 15 | warn " #{File.basename($0)} old new > output.html" 16 | exit 127 17 | end 18 | 19 | left = IO.read(ARGV[0]).split($/) 20 | right = IO.read(ARGV[1]).split($/) 21 | 22 | options = { :title => "diff #{ARGV[0]} #{ARGV[1]}" } 23 | 24 | htmldiff = Diff::LCS::HTMLDiff.new(left, right, options) 25 | 26 | if ARGV[2] 27 | File.open(ARGV[2], "w") do |f| 28 | htmldiff.options[:output] = f 29 | htmldiff.run 30 | end 31 | else 32 | htmldiff.run 33 | end 34 | 35 | # vim: ft=ruby 36 | -------------------------------------------------------------------------------- /bin/ldiff: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby -w 2 | # frozen_string_literal: true 3 | 4 | require 'diff/lcs' 5 | require 'diff/lcs/ldiff' 6 | 7 | exit Diff::LCS::Ldiff.run(ARGV) 8 | 9 | # vim: ft=ruby 10 | -------------------------------------------------------------------------------- /diff-lcs.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # stub: diff-lcs 1.6.2 ruby lib 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "diff-lcs".freeze 6 | s.version = "1.6.2".freeze 7 | 8 | s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= 9 | 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= 10 | s.require_paths = ["lib".freeze] 11 | s.authors = ["Austin Ziegler".freeze] 12 | s.date = "2025-05-12" 13 | 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 14 | s.email = ["halostatue@gmail.com".freeze] 15 | s.executables = ["htmldiff".freeze, "ldiff".freeze] 16 | 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, "docs/COPYING.txt".freeze, "docs/artistic.txt".freeze] 17 | 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, "docs/COPYING.txt".freeze, "docs/artistic.txt".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, "mise.toml".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] 18 | s.homepage = "https://github.com/halostatue/diff-lcs".freeze 19 | s.licenses = ["MIT".freeze, "Artistic-1.0-Perl".freeze, "GPL-2.0-or-later".freeze] 20 | s.rdoc_options = ["--main".freeze, "README.md".freeze] 21 | s.required_ruby_version = Gem::Requirement.new(">= 1.8".freeze) 22 | s.rubygems_version = "3.6.6".freeze 23 | s.summary = "Diff::LCS computes the difference between two Enumerable sequences using the McIlroy-Hunt longest common subsequence (LCS) algorithm".freeze 24 | 25 | s.specification_version = 4 26 | 27 | s.add_development_dependency(%q.freeze, ["~> 4.0".freeze]) 28 | s.add_development_dependency(%q.freeze, ["~> 2.0".freeze]) 29 | s.add_development_dependency(%q.freeze, ["~> 1.0".freeze]) 30 | s.add_development_dependency(%q.freeze, [">= 2.0".freeze, "< 4".freeze]) 31 | s.add_development_dependency(%q.freeze, [">= 10.0".freeze, "< 14".freeze]) 32 | s.add_development_dependency(%q.freeze, [">= 6.3.1".freeze, "< 7".freeze]) 33 | end 34 | -------------------------------------------------------------------------------- /docs/COPYING.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /docs/artistic.txt: -------------------------------------------------------------------------------- 1 | The "Artistic License" 2 | 3 | Preamble 4 | 5 | The intent of this document is to state the conditions under which a 6 | Package may be copied, such that the Copyright Holder maintains some 7 | semblance of artistic control over the development of the package, 8 | while giving the users of the package the right to use and distribute 9 | the Package in a more-or-less customary fashion, plus the right to make 10 | reasonable modifications. 11 | 12 | Definitions: 13 | 14 | "Package" refers to the collection of files distributed by the 15 | Copyright Holder, and derivatives of that collection of files 16 | created through textual modification. 17 | 18 | "Standard Version" refers to such a Package if it has not been 19 | modified, or has been modified in accordance with the wishes 20 | of the Copyright Holder as specified below. 21 | 22 | "Copyright Holder" is whoever is named in the copyright or 23 | copyrights for the package. 24 | 25 | "You" is you, if you're thinking about copying or distributing 26 | this Package. 27 | 28 | "Reasonable copying fee" is whatever you can justify on the 29 | basis of media cost, duplication charges, time of people involved, 30 | and so on. (You will not be required to justify it to the 31 | Copyright Holder, but only to the computing community at large 32 | as a market that must bear the fee.) 33 | 34 | "Freely Available" means that no fee is charged for the item 35 | itself, though there may be fees involved in handling the item. 36 | It also means that recipients of the item may redistribute it 37 | under the same conditions they received it. 38 | 39 | 1. You may make and give away verbatim copies of the source form of the 40 | Standard Version of this Package without restriction, provided that you 41 | duplicate all of the original copyright notices and associated disclaimers. 42 | 43 | 2. You may apply bug fixes, portability fixes and other modifications 44 | derived from the Public Domain or from the Copyright Holder. A Package 45 | modified in such a way shall still be considered the Standard Version. 46 | 47 | 3. You may otherwise modify your copy of this Package in any way, provided 48 | that you insert a prominent notice in each changed file stating how and 49 | when you changed that file, and provided that you do at least ONE of the 50 | following: 51 | 52 | a) place your modifications in the Public Domain or otherwise make them 53 | Freely Available, such as by posting said modifications to Usenet or 54 | an equivalent medium, or placing the modifications on a major archive 55 | site such as uunet.uu.net, or by allowing the Copyright Holder to include 56 | your modifications in the Standard Version of the Package. 57 | 58 | b) use the modified Package only within your corporation or organization. 59 | 60 | c) rename any non-standard executables so the names do not conflict 61 | with standard executables, which must also be provided, and provide 62 | a separate manual page for each non-standard executable that clearly 63 | documents how it differs from the Standard Version. 64 | 65 | d) make other distribution arrangements with the Copyright Holder. 66 | 67 | 4. You may distribute the programs of this Package in object code or 68 | executable form, provided that you do at least ONE of the following: 69 | 70 | a) distribute a Standard Version of the executables and library files, 71 | together with instructions (in the manual page or equivalent) on where 72 | to get the Standard Version. 73 | 74 | b) accompany the distribution with the machine-readable source of 75 | the Package with your modifications. 76 | 77 | c) give non-standard executables non-standard names, and clearly 78 | document the differences in manual pages (or equivalent), together 79 | with instructions on where to get the Standard Version. 80 | 81 | d) make other distribution arrangements with the Copyright Holder. 82 | 83 | 5. You may charge a reasonable copying fee for any distribution of this 84 | Package. You may charge any fee you choose for support of this 85 | Package. You may not charge a fee for this Package itself. However, 86 | you may distribute this Package in aggregate with other (possibly 87 | commercial) programs as part of a larger (possibly commercial) software 88 | distribution provided that you do not advertise this Package as a 89 | product of your own. You may embed this Package's interpreter within 90 | an executable of yours (by linking); this shall be construed as a mere 91 | form of aggregation, provided that the complete Standard Version of the 92 | interpreter is so embedded. 93 | 94 | 6. The scripts and library files supplied as input to or produced as 95 | output from the programs of this Package do not automatically fall 96 | under the copyright of this Package, but belong to whoever generated 97 | them, and may be sold commercially, and may be aggregated with this 98 | Package. If such scripts or library files are aggregated with this 99 | Package via the so-called "undump" or "unexec" methods of producing a 100 | binary executable image, then distribution of such an image shall 101 | neither be construed as a distribution of this Package nor shall it 102 | fall under the restrictions of Paragraphs 3 and 4, provided that you do 103 | not represent such an executable image as a Standard Version of this 104 | Package. 105 | 106 | 7. C subroutines (or comparably compiled subroutines in other 107 | languages) supplied by you and linked into this Package in order to 108 | emulate subroutines and variables of the language defined by this 109 | Package shall not be considered part of this Package, but are the 110 | equivalent of input as in Paragraph 6, provided these subroutines do 111 | not change the language in any way that would cause it to fail the 112 | regression tests for the language. 113 | 114 | 8. Aggregation of this Package with a commercial distribution is always 115 | permitted provided that the use of this Package is embedded; that is, 116 | when no overt attempt is made to make this Package's interfaces visible 117 | to the end user of the commercial distribution. Such use shall not be 118 | construed as a distribution of this Package. 119 | 120 | 9. The name of the Copyright Holder may not be used to endorse or promote 121 | products derived from this software without specific prior written permission. 122 | 123 | 10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 124 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 125 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. 126 | 127 | The End 128 | -------------------------------------------------------------------------------- /lib/diff-lcs.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "diff/lcs" 4 | -------------------------------------------------------------------------------- /lib/diff/lcs/array.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "diff/lcs" 4 | 5 | class Array 6 | include Diff::LCS 7 | end 8 | -------------------------------------------------------------------------------- /lib/diff/lcs/backports.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | unless 0.respond_to?(:positive?) 4 | class Fixnum # standard:disable Lint/UnifiedInteger 5 | def positive? 6 | self > 0 7 | end 8 | 9 | def negative? 10 | self < 0 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/diff/lcs/block.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # A block is an operation removing, adding, or changing a group of items. 4 | # Basically, this is just a list of changes, where each change adds or 5 | # deletes a single item. Used by bin/ldiff. 6 | class Diff::LCS::Block 7 | attr_reader :changes, :insert, :remove 8 | 9 | def initialize(chunk) 10 | @changes = [] 11 | @insert = [] 12 | @remove = [] 13 | 14 | chunk.each do |item| 15 | @changes << item 16 | @remove << item if item.deleting? 17 | @insert << item if item.adding? 18 | end 19 | end 20 | 21 | def diff_size 22 | @insert.size - @remove.size 23 | end 24 | 25 | def op 26 | case [@remove.empty?, @insert.empty?] 27 | when [false, false] 28 | "!" 29 | when [false, true] 30 | "-" 31 | when [true, false] 32 | "+" 33 | else # [true, true] 34 | "^" 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/diff/lcs/callbacks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "diff/lcs/change" 4 | 5 | module Diff::LCS 6 | # This callback object implements the default set of callback events, 7 | # which only returns the event itself. Note that #finished_a and 8 | # #finished_b are not implemented -- I haven't yet figured out where they 9 | # would be useful. 10 | # 11 | # Note that this is intended to be called as is, e.g., 12 | # 13 | # Diff::LCS.LCS(seq1, seq2, Diff::LCS::DefaultCallbacks) 14 | class DefaultCallbacks 15 | class << self 16 | # Called when two items match. 17 | def match(event) 18 | event 19 | end 20 | 21 | # Called when the old value is discarded in favour of the new value. 22 | def discard_a(event) 23 | event 24 | end 25 | 26 | # Called when the new value is discarded in favour of the old value. 27 | def discard_b(event) 28 | event 29 | end 30 | 31 | # Called when both the old and new values have changed. 32 | def change(event) 33 | event 34 | end 35 | 36 | private :new 37 | end 38 | end 39 | 40 | # An alias for DefaultCallbacks that is used in 41 | # Diff::LCS#traverse_sequences. 42 | # 43 | # Diff::LCS.LCS(seq1, seq2, Diff::LCS::SequenceCallbacks) 44 | SequenceCallbacks = DefaultCallbacks 45 | 46 | # An alias for DefaultCallbacks that is used in 47 | # Diff::LCS#traverse_balanced. 48 | # 49 | # Diff::LCS.LCS(seq1, seq2, Diff::LCS::BalancedCallbacks) 50 | BalancedCallbacks = DefaultCallbacks 51 | 52 | def self.callbacks_for(callbacks) 53 | callbacks.new 54 | rescue 55 | callbacks 56 | end 57 | end 58 | 59 | # This will produce a compound array of simple diff change objects. Each 60 | # element in the #diffs array is a +hunk+ or +hunk+ array, where each 61 | # element in each +hunk+ array is a single Change object representing the 62 | # addition or removal of a single element from one of the two tested 63 | # sequences. The +hunk+ provides the full context for the changes. 64 | # 65 | # diffs = Diff::LCS.diff(seq1, seq2) 66 | # # This example shows a simplified array format. 67 | # # [ [ [ '-', 0, 'a' ] ], # 1 68 | # # [ [ '+', 2, 'd' ] ], # 2 69 | # # [ [ '-', 4, 'h' ], # 3 70 | # # [ '+', 4, 'f' ] ], 71 | # # [ [ '+', 6, 'k' ] ], # 4 72 | # # [ [ '-', 8, 'n' ], # 5 73 | # # [ '-', 9, 'p' ], 74 | # # [ '+', 9, 'r' ], 75 | # # [ '+', 10, 's' ], 76 | # # [ '+', 11, 't' ] ] ] 77 | # 78 | # There are five hunks here. The first hunk says that the +a+ at position 0 79 | # of the first sequence should be deleted ('-'). The second hunk 80 | # says that the +d+ at position 2 of the second sequence should be inserted 81 | # ('+'). The third hunk says that the +h+ at position 4 of the 82 | # first sequence should be removed and replaced with the +f+ from position 4 83 | # of the second sequence. The other two hunks are described similarly. 84 | # 85 | # === Use 86 | # 87 | # This callback object must be initialised and is used by the Diff::LCS#diff 88 | # method. 89 | # 90 | # cbo = Diff::LCS::DiffCallbacks.new 91 | # Diff::LCS.LCS(seq1, seq2, cbo) 92 | # cbo.finish 93 | # 94 | # Note that the call to #finish is absolutely necessary, or the last set of 95 | # changes will not be visible. Alternatively, can be used as: 96 | # 97 | # cbo = Diff::LCS::DiffCallbacks.new { |tcbo| Diff::LCS.LCS(seq1, seq2, tcbo) } 98 | # 99 | # The necessary #finish call will be made. 100 | # 101 | # === Simplified Array Format 102 | # 103 | # The simplified array format used in the example above can be obtained 104 | # with: 105 | # 106 | # require 'pp' 107 | # pp diffs.map { |e| e.map { |f| f.to_a } } 108 | class Diff::LCS::DiffCallbacks 109 | # Returns the difference set collected during the diff process. 110 | attr_reader :diffs 111 | 112 | def initialize # :yields: self 113 | @hunk = [] 114 | @diffs = [] 115 | 116 | return unless block_given? 117 | 118 | begin 119 | yield self 120 | ensure 121 | finish 122 | end 123 | end 124 | 125 | # Finalizes the diff process. If an unprocessed hunk still exists, then it 126 | # is appended to the diff list. 127 | def finish 128 | finish_hunk 129 | end 130 | 131 | def match(_event) 132 | finish_hunk 133 | end 134 | 135 | def discard_a(event) 136 | @hunk << Diff::LCS::Change.new("-", event.old_position, event.old_element) 137 | end 138 | 139 | def discard_b(event) 140 | @hunk << Diff::LCS::Change.new("+", event.new_position, event.new_element) 141 | end 142 | 143 | def finish_hunk 144 | @diffs << @hunk unless @hunk.empty? 145 | @hunk = [] 146 | end 147 | private :finish_hunk 148 | end 149 | 150 | # This will produce a compound array of contextual diff change objects. Each 151 | # element in the #diffs array is a "hunk" array, where each element in each 152 | # "hunk" array is a single change. Each change is a Diff::LCS::ContextChange 153 | # that contains both the old index and new index values for the change. The 154 | # "hunk" provides the full context for the changes. Both old and new objects 155 | # will be presented for changed objects. +nil+ will be substituted for a 156 | # discarded object. 157 | # 158 | # seq1 = %w(a b c e h j l m n p) 159 | # seq2 = %w(b c d e f j k l m r s t) 160 | # 161 | # diffs = Diff::LCS.diff(seq1, seq2, Diff::LCS::ContextDiffCallbacks) 162 | # # This example shows a simplified array format. 163 | # # [ [ [ '-', [ 0, 'a' ], [ 0, nil ] ] ], # 1 164 | # # [ [ '+', [ 3, nil ], [ 2, 'd' ] ] ], # 2 165 | # # [ [ '-', [ 4, 'h' ], [ 4, nil ] ], # 3 166 | # # [ '+', [ 5, nil ], [ 4, 'f' ] ] ], 167 | # # [ [ '+', [ 6, nil ], [ 6, 'k' ] ] ], # 4 168 | # # [ [ '-', [ 8, 'n' ], [ 9, nil ] ], # 5 169 | # # [ '+', [ 9, nil ], [ 9, 'r' ] ], 170 | # # [ '-', [ 9, 'p' ], [ 10, nil ] ], 171 | # # [ '+', [ 10, nil ], [ 10, 's' ] ], 172 | # # [ '+', [ 10, nil ], [ 11, 't' ] ] ] ] 173 | # 174 | # The five hunks shown are comprised of individual changes; if there is a 175 | # related set of changes, they are still shown individually. 176 | # 177 | # This callback can also be used with Diff::LCS#sdiff, which will produce 178 | # results like: 179 | # 180 | # diffs = Diff::LCS.sdiff(seq1, seq2, Diff::LCS::ContextCallbacks) 181 | # # This example shows a simplified array format. 182 | # # [ [ [ "-", [ 0, "a" ], [ 0, nil ] ] ], # 1 183 | # # [ [ "+", [ 3, nil ], [ 2, "d" ] ] ], # 2 184 | # # [ [ "!", [ 4, "h" ], [ 4, "f" ] ] ], # 3 185 | # # [ [ "+", [ 6, nil ], [ 6, "k" ] ] ], # 4 186 | # # [ [ "!", [ 8, "n" ], [ 9, "r" ] ], # 5 187 | # # [ "!", [ 9, "p" ], [ 10, "s" ] ], 188 | # # [ "+", [ 10, nil ], [ 11, "t" ] ] ] ] 189 | # 190 | # The five hunks are still present, but are significantly shorter in total 191 | # presentation, because changed items are shown as changes ("!") instead of 192 | # potentially "mismatched" pairs of additions and deletions. 193 | # 194 | # The result of this operation is similar to that of 195 | # Diff::LCS::SDiffCallbacks. They may be compared as: 196 | # 197 | # s = Diff::LCS.sdiff(seq1, seq2).reject { |e| e.action == "=" } 198 | # c = Diff::LCS.sdiff(seq1, seq2, Diff::LCS::ContextDiffCallbacks).flatten(1) 199 | # 200 | # s == c # -> true 201 | # 202 | # === Use 203 | # 204 | # This callback object must be initialised and can be used by the 205 | # Diff::LCS#diff or Diff::LCS#sdiff methods. 206 | # 207 | # cbo = Diff::LCS::ContextDiffCallbacks.new 208 | # Diff::LCS.LCS(seq1, seq2, cbo) 209 | # cbo.finish 210 | # 211 | # Note that the call to #finish is absolutely necessary, or the last set of 212 | # changes will not be visible. Alternatively, can be used as: 213 | # 214 | # cbo = Diff::LCS::ContextDiffCallbacks.new { |tcbo| Diff::LCS.LCS(seq1, seq2, tcbo) } 215 | # 216 | # The necessary #finish call will be made. 217 | # 218 | # === Simplified Array Format 219 | # 220 | # The simplified array format used in the example above can be obtained 221 | # with: 222 | # 223 | # require 'pp' 224 | # pp diffs.map { |e| e.map { |f| f.to_a } } 225 | class Diff::LCS::ContextDiffCallbacks < Diff::LCS::DiffCallbacks 226 | def discard_a(event) 227 | @hunk << Diff::LCS::ContextChange.simplify(event) 228 | end 229 | 230 | def discard_b(event) 231 | @hunk << Diff::LCS::ContextChange.simplify(event) 232 | end 233 | 234 | def change(event) 235 | @hunk << Diff::LCS::ContextChange.simplify(event) 236 | end 237 | end 238 | 239 | # This will produce a simple array of diff change objects. Each element in 240 | # the #diffs array is a single ContextChange. In the set of #diffs provided 241 | # by SDiffCallbacks, both old and new objects will be presented for both 242 | # changed and unchanged objects. +nil+ will be substituted 243 | # for a discarded object. 244 | # 245 | # The diffset produced by this callback, when provided to Diff::LCS#sdiff, 246 | # will compute and display the necessary components to show two sequences 247 | # and their minimized differences side by side, just like the Unix utility 248 | # +sdiff+. 249 | # 250 | # same same 251 | # before | after 252 | # old < - 253 | # - > new 254 | # 255 | # seq1 = %w(a b c e h j l m n p) 256 | # seq2 = %w(b c d e f j k l m r s t) 257 | # 258 | # diffs = Diff::LCS.sdiff(seq1, seq2) 259 | # # This example shows a simplified array format. 260 | # # [ [ "-", [ 0, "a"], [ 0, nil ] ], 261 | # # [ "=", [ 1, "b"], [ 0, "b" ] ], 262 | # # [ "=", [ 2, "c"], [ 1, "c" ] ], 263 | # # [ "+", [ 3, nil], [ 2, "d" ] ], 264 | # # [ "=", [ 3, "e"], [ 3, "e" ] ], 265 | # # [ "!", [ 4, "h"], [ 4, "f" ] ], 266 | # # [ "=", [ 5, "j"], [ 5, "j" ] ], 267 | # # [ "+", [ 6, nil], [ 6, "k" ] ], 268 | # # [ "=", [ 6, "l"], [ 7, "l" ] ], 269 | # # [ "=", [ 7, "m"], [ 8, "m" ] ], 270 | # # [ "!", [ 8, "n"], [ 9, "r" ] ], 271 | # # [ "!", [ 9, "p"], [ 10, "s" ] ], 272 | # # [ "+", [ 10, nil], [ 11, "t" ] ] ] 273 | # 274 | # The result of this operation is similar to that of 275 | # Diff::LCS::ContextDiffCallbacks. They may be compared as: 276 | # 277 | # s = Diff::LCS.sdiff(seq1, seq2).reject { |e| e.action == "=" } 278 | # c = Diff::LCS.sdiff(seq1, seq2, Diff::LCS::ContextDiffCallbacks).flatten(1) 279 | # 280 | # s == c # -> true 281 | # 282 | # === Use 283 | # 284 | # This callback object must be initialised and is used by the Diff::LCS#sdiff 285 | # method. 286 | # 287 | # cbo = Diff::LCS::SDiffCallbacks.new 288 | # Diff::LCS.LCS(seq1, seq2, cbo) 289 | # 290 | # As with the other initialisable callback objects, 291 | # Diff::LCS::SDiffCallbacks can be initialised with a block. As there is no 292 | # "fininishing" to be done, this has no effect on the state of the object. 293 | # 294 | # cbo = Diff::LCS::SDiffCallbacks.new { |tcbo| Diff::LCS.LCS(seq1, seq2, tcbo) } 295 | # 296 | # === Simplified Array Format 297 | # 298 | # The simplified array format used in the example above can be obtained 299 | # with: 300 | # 301 | # require 'pp' 302 | # pp diffs.map { |e| e.to_a } 303 | class Diff::LCS::SDiffCallbacks 304 | # Returns the difference set collected during the diff process. 305 | attr_reader :diffs 306 | 307 | def initialize # :yields: self 308 | @diffs = [] 309 | yield self if block_given? 310 | end 311 | 312 | def match(event) 313 | @diffs << Diff::LCS::ContextChange.simplify(event) 314 | end 315 | 316 | def discard_a(event) 317 | @diffs << Diff::LCS::ContextChange.simplify(event) 318 | end 319 | 320 | def discard_b(event) 321 | @diffs << Diff::LCS::ContextChange.simplify(event) 322 | end 323 | 324 | def change(event) 325 | @diffs << Diff::LCS::ContextChange.simplify(event) 326 | end 327 | end 328 | -------------------------------------------------------------------------------- /lib/diff/lcs/change.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Represents a simplistic (non-contextual) change. Represents the removal or 4 | # addition of an element from either the old or the new sequenced 5 | # enumerable. 6 | class Diff::LCS::Change 7 | IntClass = 1.class # Fixnum is deprecated in Ruby 2.4 # standard:disable Naming/ConstantName 8 | 9 | # The only actions valid for changes are '+' (add), '-' (delete), '=' 10 | # (no change), '!' (changed), '<' (tail changes from first sequence), or 11 | # '>' (tail changes from second sequence). The last two ('<>') are only 12 | # found with Diff::LCS::diff and Diff::LCS::sdiff. 13 | VALID_ACTIONS = %w[+ - = ! > <].freeze 14 | 15 | def self.valid_action?(action) 16 | VALID_ACTIONS.include? action 17 | end 18 | 19 | # Returns the action this Change represents. 20 | attr_reader :action 21 | 22 | # Returns the position of the Change. 23 | attr_reader :position 24 | # Returns the sequence element of the Change. 25 | attr_reader :element 26 | 27 | def initialize(*args) 28 | @action, @position, @element = *args 29 | 30 | fail "Invalid Change Action '#{@action}'" unless Diff::LCS::Change.valid_action?(@action) 31 | fail "Invalid Position Type" unless @position.is_a? IntClass 32 | end 33 | 34 | def inspect(*_args) 35 | "#<#{self.class}: #{to_a.inspect}>" 36 | end 37 | 38 | def to_a 39 | [@action, @position, @element] 40 | end 41 | 42 | alias_method :to_ary, :to_a 43 | 44 | def self.from_a(arr) 45 | arr = arr.flatten(1) 46 | case arr.size 47 | when 5 48 | Diff::LCS::ContextChange.new(*arr[0...5]) 49 | when 3 50 | Diff::LCS::Change.new(*arr[0...3]) 51 | else 52 | fail "Invalid change array format provided." 53 | end 54 | end 55 | 56 | include Comparable 57 | 58 | def ==(other) 59 | (self.class == other.class) and 60 | (action == other.action) and 61 | (position == other.position) and 62 | (element == other.element) 63 | end 64 | 65 | def <=>(other) 66 | r = action <=> other.action 67 | r = position <=> other.position if r.zero? 68 | r = element <=> other.element if r.zero? 69 | r 70 | end 71 | 72 | def adding? 73 | @action == "+" 74 | end 75 | 76 | def deleting? 77 | @action == "-" 78 | end 79 | 80 | def unchanged? 81 | @action == "=" 82 | end 83 | 84 | def changed? 85 | @action == "!" 86 | end 87 | 88 | def finished_a? 89 | @action == ">" 90 | end 91 | 92 | def finished_b? 93 | @action == "<" 94 | end 95 | end 96 | 97 | # Represents a contextual change. Contains the position and values of the 98 | # elements in the old and the new sequenced enumerables as well as the action 99 | # taken. 100 | class Diff::LCS::ContextChange < Diff::LCS::Change 101 | # We don't need these two values. 102 | undef :position 103 | undef :element 104 | 105 | # Returns the old position being changed. 106 | attr_reader :old_position 107 | # Returns the new position being changed. 108 | attr_reader :new_position 109 | # Returns the old element being changed. 110 | attr_reader :old_element 111 | # Returns the new element being changed. 112 | attr_reader :new_element 113 | 114 | def initialize(*args) 115 | @action, @old_position, @old_element, @new_position, @new_element = *args 116 | 117 | fail "Invalid Change Action '#{@action}'" unless Diff::LCS::Change.valid_action?(@action) 118 | fail "Invalid (Old) Position Type" unless @old_position.nil? || @old_position.is_a?(IntClass) 119 | fail "Invalid (New) Position Type" unless @new_position.nil? || @new_position.is_a?(IntClass) 120 | end 121 | 122 | def to_a 123 | [ 124 | @action, 125 | [@old_position, @old_element], 126 | [@new_position, @new_element] 127 | ] 128 | end 129 | 130 | alias_method :to_ary, :to_a 131 | 132 | def self.from_a(arr) 133 | Diff::LCS::Change.from_a(arr) 134 | end 135 | 136 | # Simplifies a context change for use in some diff callbacks. '<' actions 137 | # are converted to '-' and '>' actions are converted to '+'. 138 | def self.simplify(event) 139 | ea = event.to_a 140 | 141 | case ea[0] 142 | when "-" 143 | ea[2][1] = nil 144 | when "<" 145 | ea[0] = "-" 146 | ea[2][1] = nil 147 | when "+" 148 | ea[1][1] = nil 149 | when ">" 150 | ea[0] = "+" 151 | ea[1][1] = nil 152 | end 153 | 154 | Diff::LCS::ContextChange.from_a(ea) 155 | end 156 | 157 | def ==(other) 158 | (self.class == other.class) and 159 | (@action == other.action) and 160 | (@old_position == other.old_position) and 161 | (@new_position == other.new_position) and 162 | (@old_element == other.old_element) and 163 | (@new_element == other.new_element) 164 | end 165 | 166 | def <=>(other) 167 | r = @action <=> other.action 168 | r = @old_position <=> other.old_position if r.zero? 169 | r = @new_position <=> other.new_position if r.zero? 170 | r = @old_element <=> other.old_element if r.zero? 171 | r = @new_element <=> other.new_element if r.zero? 172 | r 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /lib/diff/lcs/htmldiff.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "erb" 4 | 5 | # Produce a simple HTML diff view. 6 | class Diff::LCS::HTMLDiff 7 | class << self 8 | # standard:disable ThreadSafety/ClassAndModuleAttributes 9 | attr_accessor :can_expand_tabs # :nodoc: 10 | # standard:enable ThreadSafety/ClassAndModuleAttributes 11 | end 12 | self.can_expand_tabs = true 13 | 14 | class Callbacks # :nodoc: 15 | attr_accessor :output 16 | attr_accessor :match_class 17 | attr_accessor :only_a_class 18 | attr_accessor :only_b_class 19 | 20 | def initialize(output, options = {}) 21 | @output = output 22 | options ||= {} 23 | 24 | @match_class = options[:match_class] || "match" 25 | @only_a_class = options[:only_a_class] || "only_a" 26 | @only_b_class = options[:only_b_class] || "only_b" 27 | end 28 | 29 | def htmlize(element, css_class) 30 | element = " " if element.empty? 31 | %(
#{element}
\n) 32 | end 33 | private :htmlize 34 | 35 | # This will be called with both lines are the same 36 | def match(event) 37 | @output << htmlize(event.old_element, :match_class) 38 | end 39 | 40 | # This will be called when there is a line in A that isn't in B 41 | def discard_a(event) 42 | @output << htmlize(event.old_element, :only_a_class) 43 | end 44 | 45 | # This will be called when there is a line in B that isn't in A 46 | def discard_b(event) 47 | @output << htmlize(event.new_element, :only_b_class) 48 | end 49 | end 50 | 51 | # standard:disable Style/HashSyntax 52 | DEFAULT_OPTIONS = { 53 | :expand_tabs => nil, 54 | :output => nil, 55 | :css => nil, 56 | :title => nil 57 | }.freeze 58 | # standard:enable Style/HashSyntax 59 | 60 | # standard:disable Layout/HeredocIndentation 61 | DEFAULT_CSS = <<-CSS 62 | body { margin: 0; } 63 | .diff 64 | { 65 | border: 1px solid black; 66 | margin: 1em 2em; 67 | } 68 | p 69 | { 70 | margin-left: 2em; 71 | } 72 | pre 73 | { 74 | padding-left: 1em; 75 | margin: 0; 76 | font-family: Inconsolata, Consolas, Lucida, Courier, monospaced; 77 | white-space: pre; 78 | } 79 | .match { } 80 | .only_a 81 | { 82 | background-color: #fdd; 83 | color: red; 84 | text-decoration: line-through; 85 | } 86 | .only_b 87 | { 88 | background-color: #ddf; 89 | color: blue; 90 | border-left: 3px solid blue 91 | } 92 | h1 { margin-left: 2em; } 93 | CSS 94 | # standard:enable Layout/HeredocIndentation 95 | 96 | def initialize(left, right, options = nil) 97 | @left = left 98 | @right = right 99 | @options = options 100 | 101 | @options = DEFAULT_OPTIONS.dup if @options.nil? 102 | end 103 | 104 | def verify_options 105 | @options[:expand_tabs] ||= 4 106 | @options[:expand_tabs] = 4 if @options[:expand_tabs].negative? 107 | 108 | @options[:output] ||= $stdout 109 | 110 | @options[:css] ||= DEFAULT_CSS.dup 111 | 112 | @options[:title] ||= "diff" 113 | end 114 | private :verify_options 115 | 116 | attr_reader :options 117 | 118 | def run 119 | verify_options 120 | 121 | if @options[:expand_tabs].positive? && self.class.can_expand_tabs 122 | formatter = Text::Format.new 123 | formatter.tabstop = @options[:expand_tabs] 124 | 125 | @left.map! { |line| formatter.expand(line.chomp) } 126 | @right.map! { |line| formatter.expand(line.chomp) } 127 | end 128 | 129 | @left.map! { |line| ERB::Util.html_escape(line.chomp) } 130 | @right.map! { |line| ERB::Util.html_escape(line.chomp) } 131 | 132 | # standard:disable Layout/HeredocIndentation 133 | @options[:output] << <<-OUTPUT 134 | 135 | 136 | #{@options[:title]} 137 | 140 | 141 | 142 |

#{@options[:title]}

143 |

Legend: Only in Old  144 | Only in New

145 |
146 | OUTPUT 147 | # standard:enable Layout/HeredocIndentation 148 | 149 | callbacks = Callbacks.new(@options[:output]) 150 | Diff::LCS.traverse_sequences(@left, @right, callbacks) 151 | 152 | # standard:disable Layout/HeredocIndentation 153 | @options[:output] << <<-OUTPUT 154 |
155 | 156 | 157 | OUTPUT 158 | # standard:enable Layout/HeredocIndentation 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /lib/diff/lcs/hunk.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "diff/lcs/block" 4 | 5 | # A Hunk is a group of Blocks which overlap because of the context surrounding 6 | # each block. (So if we're not using context, every hunk will contain one 7 | # block.) Used in the diff program (bin/ldiff). 8 | class Diff::LCS::Hunk 9 | OLD_DIFF_OP_ACTION = {"+" => "a", "-" => "d", "!" => "c"}.freeze # :nodoc: 10 | ED_DIFF_OP_ACTION = {"+" => "a", "-" => "d", "!" => "c"}.freeze # :nodoc: 11 | 12 | private_constant :OLD_DIFF_OP_ACTION, :ED_DIFF_OP_ACTION if respond_to?(:private_constant) 13 | 14 | # Create a hunk using references to both the old and new data, as well as the 15 | # piece of data. 16 | def initialize(data_old, data_new, piece, flag_context, file_length_difference) 17 | # At first, a hunk will have just one Block in it 18 | @blocks = [Diff::LCS::Block.new(piece)] 19 | 20 | if @blocks[0].remove.empty? && @blocks[0].insert.empty? 21 | fail "Cannot build a hunk from #{piece.inspect}; has no add or remove actions" 22 | end 23 | 24 | if String.method_defined?(:encoding) 25 | @preferred_data_encoding = data_old.fetch(0) { data_new.fetch(0) { "" } }.encoding 26 | end 27 | 28 | @data_old = data_old 29 | @data_new = data_new 30 | @old_empty = data_old.empty? || (data_old.size == 1 && data_old[0].empty?) 31 | @new_empty = data_new.empty? || (data_new.size == 1 && data_new[0].empty?) 32 | 33 | before = after = file_length_difference 34 | after += @blocks[0].diff_size 35 | @file_length_difference = after # The caller must get this manually 36 | @max_diff_size = @blocks.map { |e| e.diff_size.abs }.max 37 | 38 | # Save the start & end of each array. If the array doesn't exist (e.g., 39 | # we're only adding items in this block), then figure out the line number 40 | # based on the line number of the other file and the current difference in 41 | # file lengths. 42 | if @blocks[0].remove.empty? 43 | a1 = a2 = nil 44 | else 45 | a1 = @blocks[0].remove[0].position 46 | a2 = @blocks[0].remove[-1].position 47 | end 48 | 49 | if @blocks[0].insert.empty? 50 | b1 = b2 = nil 51 | else 52 | b1 = @blocks[0].insert[0].position 53 | b2 = @blocks[0].insert[-1].position 54 | end 55 | 56 | @start_old = a1 || (b1 - before) 57 | @start_new = b1 || (a1 + before) 58 | @end_old = a2 || (b2 - after) 59 | @end_new = b2 || (a2 + after) 60 | 61 | self.flag_context = flag_context 62 | end 63 | 64 | attr_reader :blocks 65 | attr_reader :start_old, :start_new 66 | attr_reader :end_old, :end_new 67 | attr_reader :file_length_difference 68 | 69 | # Change the "start" and "end" fields to note that context should be added 70 | # to this hunk. 71 | attr_accessor :flag_context 72 | undef :flag_context= 73 | def flag_context=(context) # :nodoc: # standard:disable Lint/DuplicateMethods 74 | return if context.nil? || context.zero? 75 | 76 | add_start = (context > @start_old) ? @start_old : context 77 | 78 | @start_old -= add_start 79 | @start_new -= add_start 80 | 81 | old_size = @data_old.size 82 | 83 | add_end = 84 | if (@end_old + context) >= old_size 85 | old_size - @end_old - 1 86 | else 87 | context 88 | end 89 | 90 | @end_old += add_end 91 | @end_new += add_end 92 | end 93 | 94 | # Merges this hunk and the provided hunk together if they overlap. Returns 95 | # a truthy value so that if there is no overlap, you can know the merge 96 | # was skipped. 97 | def merge(hunk) 98 | return unless overlaps?(hunk) 99 | 100 | @start_old = hunk.start_old 101 | @start_new = hunk.start_new 102 | blocks.unshift(*hunk.blocks) 103 | end 104 | alias_method :unshift, :merge 105 | 106 | # Determines whether there is an overlap between this hunk and the 107 | # provided hunk. This will be true if the difference between the two hunks 108 | # start or end positions is within one position of each other. 109 | def overlaps?(hunk) 110 | hunk and (((@start_old - hunk.end_old) <= 1) or 111 | ((@start_new - hunk.end_new) <= 1)) 112 | end 113 | 114 | # Returns a diff string based on a format. 115 | def diff(format, last = false) 116 | case format 117 | when :old 118 | old_diff(last) 119 | when :unified 120 | unified_diff(last) 121 | when :context 122 | context_diff(last) 123 | when :ed 124 | self 125 | when :reverse_ed, :ed_finish 126 | ed_diff(format, last) 127 | else 128 | fail "Unknown diff format #{format}." 129 | end 130 | end 131 | 132 | # Note that an old diff can't have any context. Therefore, we know that 133 | # there's only one block in the hunk. 134 | def old_diff(last = false) 135 | warn "Expecting only one block in an old diff hunk!" if @blocks.size > 1 136 | 137 | block = @blocks[0] 138 | 139 | if last 140 | old_missing_newline = !@old_empty && missing_last_newline?(@data_old) 141 | new_missing_newline = !@new_empty && missing_last_newline?(@data_new) 142 | end 143 | 144 | # Calculate item number range. Old diff range is just like a context 145 | # diff range, except the ranges are on one line with the action between 146 | # them. 147 | s = encode("#{context_range(:old, ",")}#{OLD_DIFF_OP_ACTION[block.op]}#{context_range(:new, ",")}\n") 148 | # If removing anything, just print out all the remove lines in the hunk 149 | # which is just all the remove lines in the block. 150 | unless block.remove.empty? 151 | @data_old[@start_old..@end_old].each { |e| s << encode("< ") + e.chomp + encode("\n") } 152 | end 153 | 154 | s << encode("\\ No newline at end of file\n") if old_missing_newline && !new_missing_newline 155 | s << encode("---\n") if block.op == "!" 156 | 157 | unless block.insert.empty? 158 | @data_new[@start_new..@end_new].each { |e| s << encode("> ") + e.chomp + encode("\n") } 159 | end 160 | 161 | s << encode("\\ No newline at end of file\n") if new_missing_newline && !old_missing_newline 162 | 163 | s 164 | end 165 | private :old_diff 166 | 167 | def unified_diff(last = false) 168 | # Calculate item number range. 169 | s = encode("@@ -#{unified_range(:old)} +#{unified_range(:new)} @@\n") 170 | 171 | # Outlist starts containing the hunk of the old file. Removing an item 172 | # just means putting a '-' in front of it. Inserting an item requires 173 | # getting it from the new file and splicing it in. We splice in 174 | # +num_added+ items. Remove blocks use +num_added+ because splicing 175 | # changed the length of outlist. 176 | # 177 | # We remove +num_removed+ items. Insert blocks use +num_removed+ 178 | # because their item numbers -- corresponding to positions in the NEW 179 | # file -- don't take removed items into account. 180 | lo, hi, num_added, num_removed = @start_old, @end_old, 0, 0 181 | 182 | # standard:disable Performance/UnfreezeString 183 | outlist = @data_old[lo..hi].map { |e| String.new("#{encode(" ")}#{e.chomp}") } 184 | # standard:enable Performance/UnfreezeString 185 | 186 | last_block = blocks[-1] 187 | 188 | if last 189 | old_missing_newline = !@old_empty && missing_last_newline?(@data_old) 190 | new_missing_newline = !@new_empty && missing_last_newline?(@data_new) 191 | end 192 | 193 | @blocks.each do |block| 194 | block.remove.each do |item| 195 | op = item.action.to_s # - 196 | offset = item.position - lo + num_added 197 | outlist[offset][0, 1] = encode(op) 198 | num_removed += 1 199 | end 200 | 201 | if last && block == last_block && old_missing_newline && !new_missing_newline 202 | outlist << encode('\\ No newline at end of file') 203 | num_removed += 1 204 | end 205 | 206 | block.insert.each do |item| 207 | op = item.action.to_s # + 208 | offset = item.position - @start_new + num_removed 209 | outlist[offset, 0] = encode(op) + @data_new[item.position].chomp 210 | num_added += 1 211 | end 212 | end 213 | 214 | outlist << encode('\\ No newline at end of file') if last && new_missing_newline 215 | 216 | s << outlist.join(encode("\n")) 217 | 218 | s 219 | end 220 | private :unified_diff 221 | 222 | def context_diff(last = false) 223 | s = encode("***************\n") 224 | s << encode("*** #{context_range(:old, ",")} ****\n") 225 | r = context_range(:new, ",") 226 | 227 | if last 228 | old_missing_newline = missing_last_newline?(@data_old) 229 | new_missing_newline = missing_last_newline?(@data_new) 230 | end 231 | 232 | # Print out file 1 part for each block in context diff format if there 233 | # are any blocks that remove items 234 | lo, hi = @start_old, @end_old 235 | removes = @blocks.reject { |e| e.remove.empty? } 236 | 237 | unless removes.empty? 238 | # standard:disable Performance/UnfreezeString 239 | outlist = @data_old[lo..hi].map { |e| String.new("#{encode(" ")}#{e.chomp}") } 240 | # standard:enable Performance/UnfreezeString 241 | 242 | last_block = removes[-1] 243 | 244 | removes.each do |block| 245 | block.remove.each do |item| 246 | outlist[item.position - lo][0, 1] = encode(block.op) # - or ! 247 | end 248 | 249 | if last && block == last_block && old_missing_newline 250 | outlist << encode('\\ No newline at end of file') 251 | end 252 | end 253 | 254 | s << outlist.join(encode("\n")) << encode("\n") 255 | end 256 | 257 | s << encode("--- #{r} ----\n") 258 | lo, hi = @start_new, @end_new 259 | inserts = @blocks.reject { |e| e.insert.empty? } 260 | 261 | unless inserts.empty? 262 | # standard:disable Performance/UnfreezeString 263 | outlist = @data_new[lo..hi].map { |e| String.new("#{encode(" ")}#{e.chomp}") } 264 | # standard:enable Performance/UnfreezeString 265 | 266 | last_block = inserts[-1] 267 | 268 | inserts.each do |block| 269 | block.insert.each do |item| 270 | outlist[item.position - lo][0, 1] = encode(block.op) # + or ! 271 | end 272 | 273 | if last && block == last_block && new_missing_newline 274 | outlist << encode('\\ No newline at end of file') 275 | end 276 | end 277 | s << outlist.join(encode("\n")) 278 | end 279 | 280 | s 281 | end 282 | private :context_diff 283 | 284 | def ed_diff(format, last) 285 | warn "Expecting only one block in an old diff hunk!" if @blocks.size > 1 286 | if last 287 | # ed script doesn't support well incomplete lines 288 | warn ": No newline at end of file\n" if !@old_empty && missing_last_newline?(@data_old) 289 | warn ": No newline at end of file\n" if !@new_empty && missing_last_newline?(@data_new) 290 | 291 | if @blocks[0].op == "!" 292 | return +"" if @blocks[0].changes[0].element == @blocks[0].changes[1].element + "\n" 293 | return +"" if @blocks[0].changes[0].element + "\n" == @blocks[0].changes[1].element 294 | end 295 | end 296 | 297 | s = 298 | if format == :reverse_ed 299 | encode("#{ED_DIFF_OP_ACTION[@blocks[0].op]}#{context_range(:old, " ")}\n") 300 | else 301 | encode("#{context_range(:old, ",")}#{ED_DIFF_OP_ACTION[@blocks[0].op]}\n") 302 | end 303 | 304 | unless @blocks[0].insert.empty? 305 | @data_new[@start_new..@end_new].each do |e| 306 | s << e.chomp + encode("\n") 307 | end 308 | s << encode(".\n") 309 | end 310 | s 311 | end 312 | private :ed_diff 313 | 314 | # Generate a range of item numbers to print. Only print 1 number if the 315 | # range has only one item in it. Otherwise, it's 'start,end' 316 | def context_range(mode, op) 317 | case mode 318 | when :old 319 | s, e = (@start_old + 1), (@end_old + 1) 320 | when :new 321 | s, e = (@start_new + 1), (@end_new + 1) 322 | end 323 | 324 | (s < e) ? "#{s}#{op}#{e}" : e.to_s 325 | end 326 | private :context_range 327 | 328 | # Generate a range of item numbers to print for unified diff. Print number 329 | # where block starts, followed by number of lines in the block 330 | # (don't print number of lines if it's 1) 331 | def unified_range(mode) 332 | case mode 333 | when :old 334 | return "0,0" if @old_empty 335 | s, e = (@start_old + 1), (@end_old + 1) 336 | when :new 337 | return "0,0" if @new_empty 338 | s, e = (@start_new + 1), (@end_new + 1) 339 | end 340 | 341 | length = e - s + 1 342 | 343 | (length <= 1) ? e.to_s : "#{s},#{length}" 344 | end 345 | private :unified_range 346 | 347 | def missing_last_newline?(data) 348 | newline = encode("\n") 349 | 350 | if data[-2] 351 | data[-2].end_with?(newline) && !data[-1].end_with?(newline) 352 | elsif data[-1] 353 | !data[-1].end_with?(newline) 354 | else 355 | true 356 | end 357 | end 358 | 359 | if String.method_defined?(:encoding) 360 | def encode(literal, target_encoding = @preferred_data_encoding) 361 | literal.encode target_encoding 362 | end 363 | 364 | def encode_as(string, *args) 365 | args.map { |arg| arg.encode(string.encoding) } 366 | end 367 | else 368 | def encode(literal, _target_encoding = nil) 369 | literal 370 | end 371 | 372 | def encode_as(_string, *args) 373 | args 374 | end 375 | end 376 | 377 | private :encode 378 | private :encode_as 379 | end 380 | -------------------------------------------------------------------------------- /lib/diff/lcs/internals.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class << Diff::LCS 4 | def diff_traversal(method, seq1, seq2, callbacks, &block) 5 | callbacks = callbacks_for(callbacks) 6 | case method 7 | when :diff 8 | traverse_sequences(seq1, seq2, callbacks) 9 | when :sdiff 10 | traverse_balanced(seq1, seq2, callbacks) 11 | end 12 | callbacks.finish if callbacks.respond_to? :finish 13 | 14 | if block 15 | callbacks.diffs.map do |hunk| 16 | if hunk.is_a? Array 17 | hunk.map { |hunk_block| block[hunk_block] } 18 | else 19 | block[hunk] 20 | end 21 | end 22 | else 23 | callbacks.diffs 24 | end 25 | end 26 | private :diff_traversal 27 | end 28 | 29 | module Diff::LCS::Internals # :nodoc: 30 | end 31 | 32 | class << Diff::LCS::Internals 33 | # Compute the longest common subsequence between the sequenced 34 | # Enumerables +a+ and +b+. The result is an array whose contents is such 35 | # that 36 | # 37 | # result = Diff::LCS::Internals.lcs(a, b) 38 | # result.each_with_index do |e, i| 39 | # assert_equal(a[i], b[e]) unless e.nil? 40 | # end 41 | def lcs(a, b) 42 | a_start = b_start = 0 43 | a_finish = a.size - 1 44 | b_finish = b.size - 1 45 | vector = [] 46 | 47 | # Collect any common elements at the beginning... 48 | while (a_start <= a_finish) && (b_start <= b_finish) && (a[a_start] == b[b_start]) 49 | vector[a_start] = b_start 50 | a_start += 1 51 | b_start += 1 52 | end 53 | 54 | # Now the end... 55 | while (a_start <= a_finish) && (b_start <= b_finish) && (a[a_finish] == b[b_finish]) 56 | vector[a_finish] = b_finish 57 | a_finish -= 1 58 | b_finish -= 1 59 | end 60 | 61 | # Now, compute the equivalence classes of positions of elements. 62 | # An explanation for how this works: https://codeforces.com/topic/92191 63 | b_matches = position_hash(b, b_start..b_finish) 64 | 65 | thresh = [] 66 | links = [] 67 | string = a.is_a?(String) 68 | 69 | (a_start..a_finish).each do |i| 70 | ai = string ? a[i, 1] : a[i] 71 | bm = b_matches[ai] 72 | k = nil 73 | bm.reverse_each do |j| 74 | # Although the threshold check is not mandatory for this to work, 75 | # it may have an optimization purpose 76 | # An attempt to remove it: https://github.com/halostatue/diff-lcs/pull/72 77 | # Why it is reintroduced: https://github.com/halostatue/diff-lcs/issues/78 78 | if k && (thresh[k] > j) && (thresh[k - 1] < j) 79 | thresh[k] = j 80 | else 81 | k = replace_next_larger(thresh, j, k) 82 | end 83 | links[k] = [k.positive? ? links[k - 1] : nil, i, j] unless k.nil? 84 | end 85 | end 86 | 87 | unless thresh.empty? 88 | link = links[thresh.size - 1] 89 | until link.nil? 90 | vector[link[1]] = link[2] 91 | link = link[0] 92 | end 93 | end 94 | 95 | vector 96 | end 97 | 98 | # This method will analyze the provided patchset to provide a single-pass 99 | # normalization (conversion of the array form of Diff::LCS::Change objects to 100 | # the object form of same) and detection of whether the patchset represents 101 | # changes to be made. 102 | def analyze_patchset(patchset, depth = 0) 103 | fail "Patchset too complex" if depth > 1 104 | 105 | has_changes = false 106 | new_patchset = [] 107 | 108 | # Format: 109 | # [ # patchset 110 | # # hunk (change) 111 | # [ # hunk 112 | # # change 113 | # ] 114 | # ] 115 | 116 | patchset.each do |hunk| 117 | case hunk 118 | when Diff::LCS::Change 119 | has_changes ||= !hunk.unchanged? 120 | new_patchset << hunk 121 | when Array 122 | # Detect if the 'hunk' is actually an array-format change object. 123 | if Diff::LCS::Change.valid_action? hunk[0] 124 | hunk = Diff::LCS::Change.from_a(hunk) 125 | has_changes ||= !hunk.unchanged? 126 | new_patchset << hunk 127 | else 128 | with_changes, hunk = analyze_patchset(hunk, depth + 1) 129 | has_changes ||= with_changes 130 | new_patchset.concat(hunk) 131 | end 132 | else 133 | fail ArgumentError, "Cannot normalise a hunk of class #{hunk.class}." 134 | end 135 | end 136 | 137 | [has_changes, new_patchset] 138 | end 139 | 140 | # Examine the patchset and the source to see in which direction the 141 | # patch should be applied. 142 | # 143 | # WARNING: By default, this examines the whole patch, so this could take 144 | # some time. This also works better with Diff::LCS::ContextChange or 145 | # Diff::LCS::Change as its source, as an array will cause the creation 146 | # of one of the above. 147 | def intuit_diff_direction(src, patchset, limit = nil) 148 | string = src.is_a?(String) 149 | count = left_match = left_miss = right_match = right_miss = 0 150 | 151 | patchset.each do |change| 152 | count += 1 153 | 154 | case change 155 | when Diff::LCS::ContextChange 156 | le = string ? src[change.old_position, 1] : src[change.old_position] 157 | re = string ? src[change.new_position, 1] : src[change.new_position] 158 | 159 | case change.action 160 | when "-" # Remove details from the old string 161 | if le == change.old_element 162 | left_match += 1 163 | else 164 | left_miss += 1 165 | end 166 | when "+" 167 | if re == change.new_element 168 | right_match += 1 169 | else 170 | right_miss += 1 171 | end 172 | when "=" 173 | left_miss += 1 if le != change.old_element 174 | right_miss += 1 if re != change.new_element 175 | when "!" 176 | if le == change.old_element 177 | left_match += 1 178 | elsif re == change.new_element 179 | right_match += 1 180 | else 181 | left_miss += 1 182 | right_miss += 1 183 | end 184 | end 185 | when Diff::LCS::Change 186 | # With a simplistic change, we can't tell the difference between 187 | # the left and right on '!' actions, so we ignore those. On '=' 188 | # actions, if there's a miss, we miss both left and right. 189 | element = string ? src[change.position, 1] : src[change.position] 190 | 191 | case change.action 192 | when "-" 193 | if element == change.element 194 | left_match += 1 195 | else 196 | left_miss += 1 197 | end 198 | when "+" 199 | if element == change.element 200 | right_match += 1 201 | else 202 | right_miss += 1 203 | end 204 | when "=" 205 | if element != change.element 206 | left_miss += 1 207 | right_miss += 1 208 | end 209 | end 210 | end 211 | 212 | break if !limit.nil? && (count > limit) 213 | end 214 | 215 | no_left = left_match.zero? && left_miss.positive? 216 | no_right = right_match.zero? && right_miss.positive? 217 | 218 | case [no_left, no_right] 219 | when [false, true] 220 | :patch 221 | when [true, false] 222 | :unpatch 223 | else 224 | case left_match <=> right_match 225 | when 1 226 | if left_miss.zero? 227 | :patch 228 | else 229 | :unpatch 230 | end 231 | when -1 232 | if right_miss.zero? 233 | :unpatch 234 | else 235 | :patch 236 | end 237 | else 238 | fail "The provided patchset does not appear to apply to the provided \ 239 | enumerable as either source or destination value." 240 | end 241 | end 242 | end 243 | 244 | # Find the place at which +value+ would normally be inserted into the 245 | # Enumerable. If that place is already occupied by +value+, do nothing 246 | # and return +nil+. If the place does not exist (i.e., it is off the end 247 | # of the Enumerable), add it to the end. Otherwise, replace the element 248 | # at that point with +value+. It is assumed that the Enumerable's values 249 | # are numeric. 250 | # 251 | # This operation preserves the sort order. 252 | def replace_next_larger(enum, value, last_index = nil) 253 | # Off the end? 254 | if enum.empty? || (value > enum[-1]) 255 | enum << value 256 | return enum.size - 1 257 | end 258 | 259 | # Binary search for the insertion point 260 | last_index ||= enum.size - 1 261 | first_index = 0 262 | while first_index <= last_index 263 | i = (first_index + last_index) >> 1 264 | 265 | found = enum[i] 266 | 267 | return nil if value == found 268 | 269 | if value > found 270 | first_index = i + 1 271 | else 272 | last_index = i - 1 273 | end 274 | end 275 | 276 | # The insertion point is in first_index; overwrite the next larger 277 | # value. 278 | enum[first_index] = value 279 | first_index 280 | end 281 | private :replace_next_larger 282 | 283 | # If +vector+ maps the matching elements of another collection onto this 284 | # Enumerable, compute the inverse of +vector+ that maps this Enumerable 285 | # onto the collection. (Currently unused.) 286 | def inverse_vector(a, vector) 287 | inverse = a.dup 288 | (0...vector.size).each do |i| 289 | inverse[vector[i]] = i unless vector[i].nil? 290 | end 291 | inverse 292 | end 293 | private :inverse_vector 294 | 295 | # Returns a hash mapping each element of an Enumerable to the set of 296 | # positions it occupies in the Enumerable, optionally restricted to the 297 | # elements specified in the range of indexes specified by +interval+. 298 | def position_hash(enum, interval) 299 | string = enum.is_a?(String) 300 | hash = Hash.new { |h, k| h[k] = [] } 301 | interval.each do |i| 302 | k = string ? enum[i, 1] : enum[i] 303 | hash[k] << i 304 | end 305 | hash 306 | end 307 | private :position_hash 308 | end 309 | -------------------------------------------------------------------------------- /lib/diff/lcs/ldiff.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "optparse" 4 | require "diff/lcs/hunk" 5 | 6 | class Diff::LCS::Ldiff # :nodoc: 7 | # standard:disable Layout/HeredocIndentation 8 | BANNER = <<-COPYRIGHT 9 | ldiff #{Diff::LCS::VERSION} 10 | Copyright 2004-2025 Austin Ziegler 11 | 12 | Part of Diff::LCS. 13 | https://github.com/halostatue/diff-lcs 14 | 15 | This program is free software. It may be redistributed and/or modified under 16 | the terms of the GPL version 2 (or later), the Perl Artistic licence, or the 17 | MIT licence. 18 | COPYRIGHT 19 | # standard:enable Layout/HeredocIndentation 20 | 21 | InputInfo = Struct.new(:filename, :data, :stat) do 22 | def initialize(filename) 23 | super(filename, ::File.read(filename), ::File.stat(filename)) 24 | end 25 | end 26 | 27 | attr_reader :format, :lines # :nodoc: 28 | attr_reader :file_old, :file_new # :nodoc: 29 | attr_reader :data_old, :data_new # :nodoc: 30 | 31 | def self.run(args, input = $stdin, output = $stdout, error = $stderr) # :nodoc: 32 | new.run(args, input, output, error) 33 | end 34 | 35 | def initialize 36 | @binary = nil 37 | @format = :old 38 | @lines = 0 39 | end 40 | 41 | def run(args, _input = $stdin, output = $stdout, error = $stderr) # :nodoc: 42 | args.options do |o| 43 | o.banner = "Usage: #{File.basename($0)} [options] oldfile newfile" 44 | o.separator "" 45 | o.on( 46 | "-c", "-C", "--context [LINES]", Integer, 47 | "Displays a context diff with LINES lines", "of context. Default 3 lines." 48 | ) do |ctx| 49 | @format = :context 50 | @lines = ctx || 3 51 | end 52 | o.on( 53 | "-u", "-U", "--unified [LINES]", Integer, 54 | "Displays a unified diff with LINES lines", "of context. Default 3 lines." 55 | ) do |ctx| 56 | @format = :unified 57 | @lines = ctx || 3 58 | end 59 | o.on("-e", "Creates an 'ed' script to change", "oldfile to newfile.") do |_ctx| 60 | @format = :ed 61 | end 62 | o.on("-f", "Creates an 'ed' script to change", "oldfile to newfile in reverse order.") do |_ctx| 63 | @format = :reverse_ed 64 | end 65 | o.on( 66 | "-a", "--text", 67 | "Treat the files as text and compare them", "line-by-line, even if they do not seem", "to be text." 68 | ) do |_txt| 69 | @binary = false 70 | end 71 | o.on("--binary", "Treats the files as binary.") do |_bin| 72 | @binary = true 73 | end 74 | o.on("-q", "--brief", "Report only whether or not the files", "differ, not the details.") do |_ctx| 75 | @format = :report 76 | end 77 | o.on_tail("--help", "Shows this text.") do 78 | error << o 79 | return 0 80 | end 81 | o.on_tail("--version", "Shows the version of Diff::LCS.") do 82 | error << Diff::LCS::Ldiff::BANNER 83 | return 0 84 | end 85 | o.on_tail "" 86 | o.on_tail 'By default, runs produces an "old-style" diff, with output like UNIX diff.' 87 | o.parse! 88 | end 89 | 90 | unless args.size == 2 91 | error << args.options 92 | return 127 93 | end 94 | 95 | # Defaults are for old-style diff 96 | @format ||= :old 97 | @lines ||= 0 98 | 99 | file_old, file_new = *ARGV 100 | diff?( 101 | InputInfo.new(file_old), 102 | InputInfo.new(file_new), 103 | @format, 104 | output, 105 | binary: @binary, 106 | lines: @lines 107 | ) ? 1 : 0 108 | end 109 | 110 | def diff?(info_old, info_new, format, output, binary: nil, lines: 0) 111 | case format 112 | when :context 113 | char_old = "*" * 3 114 | char_new = "-" * 3 115 | when :unified 116 | char_old = "-" * 3 117 | char_new = "+" * 3 118 | end 119 | 120 | # After we've read up to a certain point in each file, the number of 121 | # items we've read from each file will differ by FLD (could be 0). 122 | file_length_difference = 0 123 | 124 | # Test binary status 125 | if binary.nil? 126 | old_bin = info_old.data[0, 4096].include?("\0") 127 | new_bin = info_new.data[0, 4096].include?("\0") 128 | binary = old_bin || new_bin 129 | end 130 | 131 | # diff yields lots of pieces, each of which is basically a Block object 132 | if binary 133 | has_diffs = (info_old.data != info_new.data) 134 | if format != :report 135 | if has_diffs 136 | output << "Binary files #{info_old.filename} and #{info_new.filename} differ\n" 137 | return true 138 | end 139 | return false 140 | end 141 | else 142 | data_old = info_old.data.lines.to_a 143 | data_new = info_new.data.lines.to_a 144 | diffs = Diff::LCS.diff(data_old, data_new) 145 | return false if diffs.empty? 146 | end 147 | 148 | case format 149 | when :report 150 | output << "Files #{info_old.filename} and #{info_new.filename} differ\n" 151 | return true 152 | when :unified, :context 153 | ft = info_old.stat.mtime.localtime.strftime("%Y-%m-%d %H:%M:%S.000000000 %z") 154 | output << "#{char_old} #{info_old.filename}\t#{ft}\n" 155 | ft = info_new.stat.mtime.localtime.strftime("%Y-%m-%d %H:%M:%S.000000000 %z") 156 | output << "#{char_new} #{info_new.filename}\t#{ft}\n" 157 | when :ed 158 | real_output = output 159 | output = [] 160 | end 161 | 162 | # Loop over hunks. If a hunk overlaps with the last hunk, join them. 163 | # Otherwise, print out the old one. 164 | oldhunk = hunk = nil 165 | diffs.each do |piece| 166 | begin 167 | hunk = Diff::LCS::Hunk.new(data_old, data_new, piece, lines, file_length_difference) 168 | file_length_difference = hunk.file_length_difference 169 | 170 | next unless oldhunk 171 | next if lines.positive? && hunk.merge(oldhunk) 172 | 173 | output << oldhunk.diff(format) 174 | output << "\n" if format == :unified 175 | ensure 176 | oldhunk = hunk 177 | end 178 | end 179 | 180 | last = oldhunk.diff(format, true) 181 | last << "\n" unless last.is_a?(Diff::LCS::Hunk) || last.empty? || last.end_with?("\n") 182 | 183 | output << last 184 | 185 | output.reverse_each { |e| real_output << e.diff(:ed_finish, e == output[0]) } if format == :ed 186 | 187 | true 188 | end 189 | end 190 | -------------------------------------------------------------------------------- /lib/diff/lcs/string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class String 4 | include Diff::LCS 5 | end 6 | -------------------------------------------------------------------------------- /lib/diff/lcs/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Diff 4 | module LCS 5 | VERSION = "1.6.2" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | ruby = "3.4" 3 | 4 | [env] 5 | MAINTENANCE = "true" 6 | -------------------------------------------------------------------------------- /spec/change_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe Diff::LCS::Change do 6 | describe "an add" do 7 | subject { described_class.new("+", 0, "element") } 8 | it { should_not be_deleting } 9 | it { should be_adding } 10 | it { should_not be_unchanged } 11 | it { should_not be_changed } 12 | it { should_not be_finished_a } 13 | it { should_not be_finished_b } 14 | end 15 | 16 | describe "a delete" do 17 | subject { described_class.new("-", 0, "element") } 18 | it { should be_deleting } 19 | it { should_not be_adding } 20 | it { should_not be_unchanged } 21 | it { should_not be_changed } 22 | it { should_not be_finished_a } 23 | it { should_not be_finished_b } 24 | end 25 | 26 | describe "an unchanged" do 27 | subject { described_class.new("=", 0, "element") } 28 | it { should_not be_deleting } 29 | it { should_not be_adding } 30 | it { should be_unchanged } 31 | it { should_not be_changed } 32 | it { should_not be_finished_a } 33 | it { should_not be_finished_b } 34 | end 35 | 36 | describe "a changed" do 37 | subject { described_class.new("!", 0, "element") } 38 | it { should_not be_deleting } 39 | it { should_not be_adding } 40 | it { should_not be_unchanged } 41 | it { should be_changed } 42 | it { should_not be_finished_a } 43 | it { should_not be_finished_b } 44 | end 45 | 46 | describe "a finished_a" do 47 | subject { described_class.new(">", 0, "element") } 48 | it { should_not be_deleting } 49 | it { should_not be_adding } 50 | it { should_not be_unchanged } 51 | it { should_not be_changed } 52 | it { should be_finished_a } 53 | it { should_not be_finished_b } 54 | end 55 | 56 | describe "a finished_b" do 57 | subject { described_class.new("<", 0, "element") } 58 | it { should_not be_deleting } 59 | it { should_not be_adding } 60 | it { should_not be_unchanged } 61 | it { should_not be_changed } 62 | it { should_not be_finished_a } 63 | it { should be_finished_b } 64 | end 65 | 66 | describe "as array" do 67 | it "should be converted" do 68 | action, position, element = described_class.new("!", 0, "element") 69 | expect(action).to eq "!" 70 | expect(position).to eq 0 71 | expect(element).to eq "element" 72 | end 73 | end 74 | end 75 | 76 | describe Diff::LCS::ContextChange do 77 | describe "as array" do 78 | it "should be converted" do 79 | action, (old_position, old_element), (new_position, new_element) = 80 | described_class.new("!", 1, "old_element", 2, "new_element") 81 | 82 | expect(action).to eq "!" 83 | expect(old_position).to eq 1 84 | expect(old_element).to eq "old_element" 85 | expect(new_position).to eq 2 86 | expect(new_element).to eq "new_element" 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/diff_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe Diff::LCS, ".diff" do 6 | include Diff::LCS::SpecHelper::Matchers 7 | 8 | it "correctly diffs seq1 to seq2" do 9 | diff_s1_s2 = Diff::LCS.diff(seq1, seq2) 10 | expect(change_diff(correct_forward_diff)).to eq(diff_s1_s2) 11 | end 12 | 13 | it "correctly diffs seq2 to seq1" do 14 | diff_s2_s1 = Diff::LCS.diff(seq2, seq1) 15 | expect(change_diff(correct_backward_diff)).to eq(diff_s2_s1) 16 | end 17 | 18 | it "correctly diffs against an empty sequence" do 19 | diff = Diff::LCS.diff(word_sequence, []) 20 | correct_diff = [ 21 | [ 22 | ["-", 0, "abcd"], 23 | ["-", 1, "efgh"], 24 | ["-", 2, "ijkl"], 25 | ["-", 3, "mnopqrstuvwxyz"] 26 | ] 27 | ] 28 | 29 | expect(change_diff(correct_diff)).to eq(diff) 30 | 31 | diff = Diff::LCS.diff([], word_sequence) 32 | correct_diff.each do |hunk| 33 | hunk.each { |change| change[0] = "+" } 34 | end 35 | expect(change_diff(correct_diff)).to eq(diff) 36 | end 37 | 38 | it "correctly diffs 'xx' and 'xaxb'" do 39 | left = "xx" 40 | right = "xaxb" 41 | expect(Diff::LCS.patch(left, Diff::LCS.diff(left, right))).to eq(right) 42 | end 43 | 44 | it "returns an empty diff with (hello, hello)" do 45 | expect(Diff::LCS.diff(hello, hello)).to be_empty 46 | end 47 | 48 | it "returns an empty diff with (hello_ary, hello_ary)" do 49 | expect(Diff::LCS.diff(hello_ary, hello_ary)).to be_empty 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/fixtures/123_x: -------------------------------------------------------------------------------- 1 | 123 2 | x 3 | -------------------------------------------------------------------------------- /spec/fixtures/456_x: -------------------------------------------------------------------------------- 1 | 456 2 | x 3 | -------------------------------------------------------------------------------- /spec/fixtures/aX: -------------------------------------------------------------------------------- 1 | aX 2 | -------------------------------------------------------------------------------- /spec/fixtures/bXaX: -------------------------------------------------------------------------------- 1 | bXaX 2 | -------------------------------------------------------------------------------- /spec/fixtures/ds1.csv: -------------------------------------------------------------------------------- 1 | 1,3 2 | 2,7 3 | 3,13 4 | 4,21 5 | 5,31 6 | 6,43 7 | 7,57 8 | 8,73 9 | 9,91 10 | 10,111 11 | 11,133 12 | 12,157 13 | 13,183 14 | 14,211 15 | 15,241 16 | 16,273 17 | 17,307 18 | 18,343 19 | 19,381 20 | 20,421 21 | 21,463 22 | 22,507 23 | 23,553 24 | 24,601 25 | 25,651 26 | 26,703 27 | 27,757 28 | 28,813 29 | 29,871 30 | 30,931 31 | 31,993 32 | 32,1057 33 | 33,1123 34 | 34,1191 35 | 35,1261 36 | 36,1333 37 | 37,1407 38 | 38,1483 39 | 39,1561 40 | 40,1641 41 | 41,1723 42 | 42,1807 43 | 43,1893 44 | 44,1981 45 | 45,2071 46 | 46,2163 47 | 47,2257 48 | 48,2353 49 | 49,2451 50 | 50,2500 -------------------------------------------------------------------------------- /spec/fixtures/ds2.csv: -------------------------------------------------------------------------------- 1 | 1,3 2 | 2,7 3 | 3,13 4 | 4,21 5 | 5,31 6 | 6,42 7 | 7,57 8 | 8,73 9 | 9,91 10 | 10,111 11 | 11,133 12 | 12,157 13 | 13,183 14 | 14,211 15 | 15,241 16 | 16,273 17 | 17,307 18 | 18,343 19 | 19,200 20 | 20,421 21 | 21,463 22 | 22,507 23 | 23,553 24 | 24,601 25 | 25,651 26 | 26,703 27 | 27,757 28 | 28,813 29 | 29,871 30 | 30,931 31 | 31,123 32 | 32,1057 33 | 33,1123 34 | 34,1000 35 | 35,1261 36 | 36,1333 37 | 37,1407 38 | 38,1483 39 | 39,1561 40 | 40,1641 41 | 41,1723 42 | 42,1807 43 | 43,1893 44 | 44,1981 45 | 45,2071 46 | 46,2163 47 | 47,1524 48 | 48,2353 49 | 49,2451 50 | 50,2500 51 | 51,2520 52 | -------------------------------------------------------------------------------- /spec/fixtures/empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halostatue/diff-lcs/aa87bb4f1e9e6869806e33adcb3a48402baba631/spec/fixtures/empty -------------------------------------------------------------------------------- /spec/fixtures/file1.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/fixtures/file2.bin: -------------------------------------------------------------------------------- 1 | hello -------------------------------------------------------------------------------- /spec/fixtures/four_lines: -------------------------------------------------------------------------------- 1 | one 2 | two 3 | three 4 | four 5 | -------------------------------------------------------------------------------- /spec/fixtures/four_lines_with_missing_new_line: -------------------------------------------------------------------------------- 1 | one 2 | two 3 | three 4 | four -------------------------------------------------------------------------------- /spec/fixtures/ldiff/diff.missing_new_line1-e: -------------------------------------------------------------------------------- 1 | No newline at end of file 2 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/diff.missing_new_line1-f: -------------------------------------------------------------------------------- 1 | No newline at end of file 2 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/diff.missing_new_line2-e: -------------------------------------------------------------------------------- 1 | No newline at end of file 2 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/diff.missing_new_line2-f: -------------------------------------------------------------------------------- 1 | No newline at end of file 2 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/error.diff.chef-e: -------------------------------------------------------------------------------- 1 | : No newline at end of file 2 | : No newline at end of file 3 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/error.diff.chef-f: -------------------------------------------------------------------------------- 1 | : No newline at end of file 2 | : No newline at end of file 3 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/error.diff.missing_new_line1-e: -------------------------------------------------------------------------------- 1 | : No newline at end of file 2 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/error.diff.missing_new_line1-f: -------------------------------------------------------------------------------- 1 | : No newline at end of file 2 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/error.diff.missing_new_line2-e: -------------------------------------------------------------------------------- 1 | : No newline at end of file 2 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/error.diff.missing_new_line2-f: -------------------------------------------------------------------------------- 1 | : No newline at end of file 2 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff: -------------------------------------------------------------------------------- 1 | 1c1 2 | < aX 3 | --- 4 | > bXaX 5 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff-c: -------------------------------------------------------------------------------- 1 | *** spec/fixtures/aX 2020-06-23 11:15:32.000000000 -0400 2 | --- spec/fixtures/bXaX 2020-06-23 11:15:32.000000000 -0400 3 | *************** 4 | *** 1 **** 5 | ! aX 6 | --- 1 ---- 7 | ! bXaX 8 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff-e: -------------------------------------------------------------------------------- 1 | 1c 2 | bXaX 3 | . 4 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff-f: -------------------------------------------------------------------------------- 1 | c1 2 | bXaX 3 | . 4 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff-u: -------------------------------------------------------------------------------- 1 | --- spec/fixtures/aX 2020-06-23 11:15:32.000000000 -0400 2 | +++ spec/fixtures/bXaX 2020-06-23 11:15:32.000000000 -0400 3 | @@ -1 +1 @@ 4 | -aX 5 | +bXaX 6 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.bin1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halostatue/diff-lcs/aa87bb4f1e9e6869806e33adcb3a48402baba631/spec/fixtures/ldiff/output.diff.bin1 -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.bin1-c: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halostatue/diff-lcs/aa87bb4f1e9e6869806e33adcb3a48402baba631/spec/fixtures/ldiff/output.diff.bin1-c -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.bin1-e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halostatue/diff-lcs/aa87bb4f1e9e6869806e33adcb3a48402baba631/spec/fixtures/ldiff/output.diff.bin1-e -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.bin1-f: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halostatue/diff-lcs/aa87bb4f1e9e6869806e33adcb3a48402baba631/spec/fixtures/ldiff/output.diff.bin1-f -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.bin1-u: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halostatue/diff-lcs/aa87bb4f1e9e6869806e33adcb3a48402baba631/spec/fixtures/ldiff/output.diff.bin1-u -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.bin2: -------------------------------------------------------------------------------- 1 | Binary files spec/fixtures/file1.bin and spec/fixtures/file2.bin differ 2 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.bin2-c: -------------------------------------------------------------------------------- 1 | Binary files spec/fixtures/file1.bin and spec/fixtures/file2.bin differ 2 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.bin2-e: -------------------------------------------------------------------------------- 1 | Binary files spec/fixtures/file1.bin and spec/fixtures/file2.bin differ 2 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.bin2-f: -------------------------------------------------------------------------------- 1 | Binary files spec/fixtures/file1.bin and spec/fixtures/file2.bin differ 2 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.bin2-u: -------------------------------------------------------------------------------- 1 | Binary files spec/fixtures/file1.bin and spec/fixtures/file2.bin differ 2 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.chef: -------------------------------------------------------------------------------- 1 | 3c3 2 | < "description": "hi" 3 | --- 4 | > "description": "lo" 5 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.chef-c: -------------------------------------------------------------------------------- 1 | *** spec/fixtures/old-chef 2020-06-23 23:18:20.000000000 -0400 2 | --- spec/fixtures/new-chef 2020-06-23 23:18:20.000000000 -0400 3 | *************** 4 | *** 1,4 **** 5 | { 6 | "name": "x", 7 | ! "description": "hi" 8 | } 9 | \ No newline at end of file 10 | --- 1,4 ---- 11 | { 12 | "name": "x", 13 | ! "description": "lo" 14 | } 15 | \ No newline at end of file 16 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.chef-e: -------------------------------------------------------------------------------- 1 | 3c 2 | "description": "lo" 3 | . 4 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.chef-f: -------------------------------------------------------------------------------- 1 | c3 2 | "description": "lo" 3 | . 4 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.chef-u: -------------------------------------------------------------------------------- 1 | --- spec/fixtures/old-chef 2020-06-23 23:18:20.000000000 -0400 2 | +++ spec/fixtures/new-chef 2020-06-23 23:18:20.000000000 -0400 3 | @@ -1,4 +1,4 @@ 4 | { 5 | "name": "x", 6 | - "description": "hi" 7 | + "description": "lo" 8 | } 9 | \ No newline at end of file 10 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.chef2: -------------------------------------------------------------------------------- 1 | 2d1 2 | < recipe[b::default] 3 | 14a14,17 4 | > recipe[o::new] 5 | > recipe[p::new] 6 | > recipe[q::new] 7 | > recipe[r::new] 8 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.chef2-c: -------------------------------------------------------------------------------- 1 | *** spec/fixtures/old-chef2 2020-06-30 09:43:35.000000000 -0400 2 | --- spec/fixtures/new-chef2 2020-06-30 09:44:32.000000000 -0400 3 | *************** 4 | *** 1,5 **** 5 | recipe[a::default] 6 | - recipe[b::default] 7 | recipe[c::default] 8 | recipe[d::default] 9 | recipe[e::default] 10 | --- 1,4 ---- 11 | *************** 12 | *** 12,14 **** 13 | --- 11,17 ---- 14 | recipe[l::default] 15 | recipe[m::default] 16 | recipe[n::default] 17 | + recipe[o::new] 18 | + recipe[p::new] 19 | + recipe[q::new] 20 | + recipe[r::new] 21 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.chef2-d: -------------------------------------------------------------------------------- 1 | d2 2 | a14 3 | recipe[o::new] 4 | recipe[p::new] 5 | recipe[q::new] 6 | recipe[r::new] 7 | . 8 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.chef2-e: -------------------------------------------------------------------------------- 1 | 14a 2 | recipe[o::new] 3 | recipe[p::new] 4 | recipe[q::new] 5 | recipe[r::new] 6 | . 7 | 2d 8 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.chef2-f: -------------------------------------------------------------------------------- 1 | d2 2 | a14 3 | recipe[o::new] 4 | recipe[p::new] 5 | recipe[q::new] 6 | recipe[r::new] 7 | . 8 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.chef2-u: -------------------------------------------------------------------------------- 1 | --- spec/fixtures/old-chef2 2020-06-30 09:43:35.000000000 -0400 2 | +++ spec/fixtures/new-chef2 2020-06-30 09:44:32.000000000 -0400 3 | @@ -1,5 +1,4 @@ 4 | recipe[a::default] 5 | -recipe[b::default] 6 | recipe[c::default] 7 | recipe[d::default] 8 | recipe[e::default] 9 | @@ -12,3 +11,7 @@ 10 | recipe[l::default] 11 | recipe[m::default] 12 | recipe[n::default] 13 | +recipe[o::new] 14 | +recipe[p::new] 15 | +recipe[q::new] 16 | +recipe[r::new] 17 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.empty.vs.four_lines: -------------------------------------------------------------------------------- 1 | 0a1,4 2 | > one 3 | > two 4 | > three 5 | > four 6 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.empty.vs.four_lines-c: -------------------------------------------------------------------------------- 1 | *** spec/fixtures/empty 2025-01-31 12:14:52.856031635 +0100 2 | --- spec/fixtures/four_lines 2025-01-31 12:13:45.476036544 +0100 3 | *************** 4 | *** 0 **** 5 | --- 1,4 ---- 6 | + one 7 | + two 8 | + three 9 | + four 10 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.empty.vs.four_lines-e: -------------------------------------------------------------------------------- 1 | 0a 2 | one 3 | two 4 | three 5 | four 6 | . 7 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.empty.vs.four_lines-f: -------------------------------------------------------------------------------- 1 | a0 2 | one 3 | two 4 | three 5 | four 6 | . 7 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.empty.vs.four_lines-u: -------------------------------------------------------------------------------- 1 | --- spec/fixtures/empty 2025-01-31 12:14:52.856031635 +0100 2 | +++ spec/fixtures/four_lines 2025-01-31 12:13:45.476036544 +0100 3 | @@ -0,0 +1,4 @@ 4 | +one 5 | +two 6 | +three 7 | +four 8 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.four_lines.vs.empty: -------------------------------------------------------------------------------- 1 | 1,4d0 2 | < one 3 | < two 4 | < three 5 | < four 6 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.four_lines.vs.empty-c: -------------------------------------------------------------------------------- 1 | *** spec/fixtures/four_lines 2025-01-31 12:13:45.476036544 +0100 2 | --- spec/fixtures/empty 2025-01-31 12:14:52.856031635 +0100 3 | *************** 4 | *** 1,4 **** 5 | - one 6 | - two 7 | - three 8 | - four 9 | --- 0 ---- 10 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.four_lines.vs.empty-e: -------------------------------------------------------------------------------- 1 | 1,4d 2 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.four_lines.vs.empty-f: -------------------------------------------------------------------------------- 1 | d1 4 2 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.four_lines.vs.empty-u: -------------------------------------------------------------------------------- 1 | --- spec/fixtures/four_lines 2025-01-31 12:13:45.476036544 +0100 2 | +++ spec/fixtures/empty 2025-01-31 12:14:52.856031635 +0100 3 | @@ -1,4 +0,0 @@ 4 | -one 5 | -two 6 | -three 7 | -four 8 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.issue95_trailing_context: -------------------------------------------------------------------------------- 1 | 1c1 2 | < 123 3 | --- 4 | > 456 5 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.issue95_trailing_context-c: -------------------------------------------------------------------------------- 1 | *** spec/fixtures/123_x 2025-01-31 17:00:17.070615716 +0100 2 | --- spec/fixtures/456_x 2025-01-31 16:58:26.380624827 +0100 3 | *************** 4 | *** 1,2 **** 5 | ! 123 6 | x 7 | --- 1,2 ---- 8 | ! 456 9 | x 10 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.issue95_trailing_context-e: -------------------------------------------------------------------------------- 1 | 1c 2 | 456 3 | . 4 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.issue95_trailing_context-f: -------------------------------------------------------------------------------- 1 | c1 2 | 456 3 | . 4 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.issue95_trailing_context-u: -------------------------------------------------------------------------------- 1 | --- spec/fixtures/123_x 2025-01-31 17:00:17.070615716 +0100 2 | +++ spec/fixtures/456_x 2025-01-31 16:58:26.380624827 +0100 3 | @@ -1,2 +1,2 @@ 4 | -123 5 | +456 6 | x 7 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.missing_new_line1: -------------------------------------------------------------------------------- 1 | 4c4 2 | < four 3 | --- 4 | > four 5 | \ No newline at end of file 6 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.missing_new_line1-c: -------------------------------------------------------------------------------- 1 | *** spec/fixtures/four_lines 2025-01-31 12:17:43.926013315 +0100 2 | --- spec/fixtures/four_lines_with_missing_new_line 2025-01-31 12:17:43.926013315 +0100 3 | *************** 4 | *** 1,4 **** 5 | one 6 | two 7 | three 8 | ! four 9 | --- 1,4 ---- 10 | one 11 | two 12 | three 13 | ! four 14 | \ No newline at end of file 15 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.missing_new_line1-e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halostatue/diff-lcs/aa87bb4f1e9e6869806e33adcb3a48402baba631/spec/fixtures/ldiff/output.diff.missing_new_line1-e -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.missing_new_line1-f: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halostatue/diff-lcs/aa87bb4f1e9e6869806e33adcb3a48402baba631/spec/fixtures/ldiff/output.diff.missing_new_line1-f -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.missing_new_line1-u: -------------------------------------------------------------------------------- 1 | --- spec/fixtures/four_lines 2025-01-31 12:17:43.926013315 +0100 2 | +++ spec/fixtures/four_lines_with_missing_new_line 2025-01-31 12:17:43.926013315 +0100 3 | @@ -1,4 +1,4 @@ 4 | one 5 | two 6 | three 7 | -four 8 | +four 9 | \ No newline at end of file 10 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.missing_new_line2: -------------------------------------------------------------------------------- 1 | 4c4 2 | < four 3 | \ No newline at end of file 4 | --- 5 | > four 6 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.missing_new_line2-c: -------------------------------------------------------------------------------- 1 | *** spec/fixtures/four_lines_with_missing_new_line 2025-01-31 12:17:43.926013315 +0100 2 | --- spec/fixtures/four_lines 2025-01-31 12:17:43.926013315 +0100 3 | *************** 4 | *** 1,4 **** 5 | one 6 | two 7 | three 8 | ! four 9 | \ No newline at end of file 10 | --- 1,4 ---- 11 | one 12 | two 13 | three 14 | ! four 15 | -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.missing_new_line2-e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halostatue/diff-lcs/aa87bb4f1e9e6869806e33adcb3a48402baba631/spec/fixtures/ldiff/output.diff.missing_new_line2-e -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.missing_new_line2-f: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halostatue/diff-lcs/aa87bb4f1e9e6869806e33adcb3a48402baba631/spec/fixtures/ldiff/output.diff.missing_new_line2-f -------------------------------------------------------------------------------- /spec/fixtures/ldiff/output.diff.missing_new_line2-u: -------------------------------------------------------------------------------- 1 | --- spec/fixtures/four_lines_with_missing_new_line 2025-01-31 12:17:43.926013315 +0100 2 | +++ spec/fixtures/four_lines 2025-01-31 12:17:43.926013315 +0100 3 | @@ -1,4 +1,4 @@ 4 | one 5 | two 6 | three 7 | -four 8 | \ No newline at end of file 9 | +four 10 | -------------------------------------------------------------------------------- /spec/fixtures/new-chef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "x", 3 | "description": "lo" 4 | } -------------------------------------------------------------------------------- /spec/fixtures/new-chef2: -------------------------------------------------------------------------------- 1 | recipe[a::default] 2 | recipe[c::default] 3 | recipe[d::default] 4 | recipe[e::default] 5 | recipe[f::default] 6 | recipe[g::default] 7 | recipe[h::default] 8 | recipe[i::default] 9 | recipe[j::default] 10 | recipe[k::default] 11 | recipe[l::default] 12 | recipe[m::default] 13 | recipe[n::default] 14 | recipe[o::new] 15 | recipe[p::new] 16 | recipe[q::new] 17 | recipe[r::new] 18 | -------------------------------------------------------------------------------- /spec/fixtures/old-chef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "x", 3 | "description": "hi" 4 | } -------------------------------------------------------------------------------- /spec/fixtures/old-chef2: -------------------------------------------------------------------------------- 1 | recipe[a::default] 2 | recipe[b::default] 3 | recipe[c::default] 4 | recipe[d::default] 5 | recipe[e::default] 6 | recipe[f::default] 7 | recipe[g::default] 8 | recipe[h::default] 9 | recipe[i::default] 10 | recipe[j::default] 11 | recipe[k::default] 12 | recipe[l::default] 13 | recipe[m::default] 14 | recipe[n::default] 15 | -------------------------------------------------------------------------------- /spec/hunk_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | if String.method_defined?(:encoding) 6 | require "diff/lcs/hunk" 7 | 8 | describe Diff::LCS::Hunk do 9 | let(:old_data) { ["Tu a un carté avec {count} itéms".encode("UTF-16LE")] } 10 | let(:new_data) { ["Tu a un carte avec {count} items".encode("UTF-16LE")] } 11 | let(:pieces) { Diff::LCS.diff old_data, new_data } 12 | let(:hunk) { Diff::LCS::Hunk.new(old_data, new_data, pieces[0], 3, 0) } 13 | 14 | it "produces a unified diff from the two pieces" do 15 | expected = <<-EXPECTED.gsub(/^\s+/, "").encode("UTF-16LE").chomp 16 | @@ -1 +1 @@ 17 | -Tu a un carté avec {count} itéms 18 | +Tu a un carte avec {count} items 19 | EXPECTED 20 | 21 | expect(hunk.diff(:unified)).to eq(expected) 22 | end 23 | 24 | it "produces a unified diff from the two pieces (last entry)" do 25 | expected = <<-EXPECTED.gsub(/^\s+/, "").encode("UTF-16LE").chomp 26 | @@ -1 +1 @@ 27 | -Tu a un carté avec {count} itéms 28 | +Tu a un carte avec {count} items 29 | \\ No newline at end of file 30 | EXPECTED 31 | 32 | expect(hunk.diff(:unified, true)).to eq(expected) 33 | end 34 | 35 | it "produces a context diff from the two pieces" do 36 | expected = <<-EXPECTED.gsub(/^\s+/, "").encode("UTF-16LE").chomp 37 | *************** 38 | *** 1 **** 39 | ! Tu a un carté avec {count} itéms 40 | --- 1 ---- 41 | ! Tu a un carte avec {count} items 42 | EXPECTED 43 | 44 | expect(hunk.diff(:context)).to eq(expected) 45 | end 46 | 47 | it "produces an old diff from the two pieces" do 48 | expected = <<-EXPECTED.gsub(/^ +/, "").encode("UTF-16LE").chomp 49 | 1c1 50 | < Tu a un carté avec {count} itéms 51 | --- 52 | > Tu a un carte avec {count} items 53 | 54 | EXPECTED 55 | 56 | expect(hunk.diff(:old)).to eq(expected) 57 | end 58 | 59 | it "produces a reverse ed diff from the two pieces" do 60 | expected = <<-EXPECTED.gsub(/^ +/, "").encode("UTF-16LE").chomp 61 | c1 62 | Tu a un carte avec {count} items 63 | . 64 | 65 | EXPECTED 66 | 67 | expect(hunk.diff(:reverse_ed)).to eq(expected) 68 | end 69 | 70 | context "with empty first data set" do 71 | let(:old_data) { [] } 72 | 73 | it "produces a unified diff" do 74 | expected = <<-EXPECTED.gsub(/^\s+/, "").encode("UTF-16LE").chomp 75 | @@ -0,0 +1 @@ 76 | +Tu a un carte avec {count} items 77 | EXPECTED 78 | 79 | expect(hunk.diff(:unified)).to eq(expected) 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/issues_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "diff/lcs/hunk" 5 | 6 | describe "Diff::LCS Issues" do 7 | include Diff::LCS::SpecHelper::Matchers 8 | 9 | describe "issue #1" do 10 | shared_examples "handles simple diffs" do |s1, s2, forward_diff| 11 | before do 12 | @diff_s1_s2 = Diff::LCS.diff(s1, s2) 13 | end 14 | 15 | it "creates the correct diff" do 16 | expect(change_diff(forward_diff)).to eq(@diff_s1_s2) 17 | end 18 | 19 | it "creates the correct patch s1->s2" do 20 | expect(Diff::LCS.patch(s1, @diff_s1_s2)).to eq(s2) 21 | end 22 | 23 | it "creates the correct patch s2->s1" do 24 | expect(Diff::LCS.patch(s2, @diff_s1_s2)).to eq(s1) 25 | end 26 | end 27 | 28 | describe "string" do 29 | it_has_behavior "handles simple diffs", "aX", "bXaX", [ 30 | [ 31 | ["+", 0, "b"], 32 | ["+", 1, "X"] 33 | ] 34 | ] 35 | it_has_behavior "handles simple diffs", "bXaX", "aX", [ 36 | [ 37 | ["-", 0, "b"], 38 | ["-", 1, "X"] 39 | ] 40 | ] 41 | end 42 | 43 | describe "array" do 44 | it_has_behavior "handles simple diffs", %w[a X], %w[b X a X], [ 45 | [ 46 | ["+", 0, "b"], 47 | ["+", 1, "X"] 48 | ] 49 | ] 50 | it_has_behavior "handles simple diffs", %w[b X a X], %w[a X], [ 51 | [ 52 | ["-", 0, "b"], 53 | ["-", 1, "X"] 54 | ] 55 | ] 56 | end 57 | end 58 | 59 | describe "issue #57" do 60 | it "should fail with a correct error" do 61 | # standard:disable Style/HashSyntax 62 | expect { 63 | actual = {:category => "app.rack.request"} 64 | expected = {:category => "rack.middleware", :title => "Anonymous Middleware"} 65 | expect(actual).to eq(expected) 66 | }.to raise_error(RSpec::Expectations::ExpectationNotMetError) 67 | # standard:enable Style/HashSyntax 68 | end 69 | end 70 | 71 | describe "issue #65" do 72 | def diff_lines(old_lines, new_lines) 73 | file_length_difference = 0 74 | previous_hunk = nil 75 | output = [] 76 | 77 | Diff::LCS.diff(old_lines, new_lines).each do |piece| 78 | hunk = Diff::LCS::Hunk.new(old_lines, new_lines, piece, 3, file_length_difference) 79 | file_length_difference = hunk.file_length_difference 80 | maybe_contiguous_hunks = previous_hunk.nil? || hunk.merge(previous_hunk) 81 | 82 | output << "#{previous_hunk.diff(:unified)}\n" unless maybe_contiguous_hunks 83 | 84 | previous_hunk = hunk 85 | end 86 | output << "#{previous_hunk.diff(:unified, true)}\n" unless previous_hunk.nil? 87 | output.join 88 | end 89 | 90 | it "should not misplace the new chunk" do 91 | old_data = [ 92 | "recipe[a::default]", "recipe[b::default]", "recipe[c::default]", 93 | "recipe[d::default]", "recipe[e::default]", "recipe[f::default]", 94 | "recipe[g::default]", "recipe[h::default]", "recipe[i::default]", 95 | "recipe[j::default]", "recipe[k::default]", "recipe[l::default]", 96 | "recipe[m::default]", "recipe[n::default]" 97 | ] 98 | 99 | new_data = [ 100 | "recipe[a::default]", "recipe[c::default]", "recipe[d::default]", 101 | "recipe[e::default]", "recipe[f::default]", "recipe[g::default]", 102 | "recipe[h::default]", "recipe[i::default]", "recipe[j::default]", 103 | "recipe[k::default]", "recipe[l::default]", "recipe[m::default]", 104 | "recipe[n::default]", "recipe[o::new]", "recipe[p::new]", 105 | "recipe[q::new]", "recipe[r::new]" 106 | ] 107 | 108 | # standard:disable Layout/HeredocIndentation 109 | expect(diff_lines(old_data, new_data)).to eq(<<-EODIFF) 110 | @@ -1,5 +1,4 @@ 111 | recipe[a::default] 112 | -recipe[b::default] 113 | recipe[c::default] 114 | recipe[d::default] 115 | recipe[e::default] 116 | @@ -12,3 +11,7 @@ 117 | recipe[l::default] 118 | recipe[m::default] 119 | recipe[n::default] 120 | +recipe[o::new] 121 | +recipe[p::new] 122 | +recipe[q::new] 123 | +recipe[r::new] 124 | EODIFF 125 | # standard:enable Layout/HeredocIndentation 126 | end 127 | end 128 | 129 | describe "issue #107 (replaces issue #60)" do 130 | it "should produce unified output with correct context" do 131 | # standard:disable Layout/HeredocIndentation 132 | old_data = <<-DATA_OLD.strip.split("\n").map(&:chomp) 133 | { 134 | "name": "x", 135 | "description": "hi" 136 | } 137 | DATA_OLD 138 | 139 | new_data = <<-DATA_NEW.strip.split("\n").map(&:chomp) 140 | { 141 | "name": "x", 142 | "description": "lo" 143 | } 144 | DATA_NEW 145 | 146 | diff = ::Diff::LCS.diff(old_data, new_data) 147 | hunk = ::Diff::LCS::Hunk.new(old_data, new_data, diff.first, 3, 0) 148 | 149 | expect(hunk.diff(:unified)).to eq(<<-EXPECTED.chomp) 150 | @@ -1,4 +1,4 @@ 151 | { 152 | "name": "x", 153 | - "description": "hi" 154 | + "description": "lo" 155 | } 156 | EXPECTED 157 | # standard:enable Layout/HeredocIndentation 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /spec/lcs_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe Diff::LCS::Internals, ".lcs" do 6 | include Diff::LCS::SpecHelper::Matchers 7 | 8 | it "returns a meaningful LCS array with (seq1, seq2)" do 9 | res = Diff::LCS::Internals.lcs(seq1, seq2) 10 | # The result of the LCS (less the +nil+ values) must be as long as the 11 | # correct result. 12 | expect(res.compact.size).to eq(correct_lcs.size) 13 | expect(res).to correctly_map_sequence(seq1).to_other_sequence(seq2) 14 | 15 | # Compact these transformations and they should be the correct LCS. 16 | x_seq1 = (0...res.size).map { |ix| res[ix] ? seq1[ix] : nil }.compact 17 | x_seq2 = (0...res.size).map { |ix| res[ix] ? seq2[res[ix]] : nil }.compact 18 | 19 | expect(x_seq1).to eq(correct_lcs) 20 | expect(x_seq2).to eq(correct_lcs) 21 | end 22 | 23 | it "returns all indexes with (hello, hello)" do 24 | expect(Diff::LCS::Internals.lcs(hello, hello)).to \ 25 | eq((0...hello.size).to_a) 26 | end 27 | 28 | it "returns all indexes with (hello_ary, hello_ary)" do 29 | expect(Diff::LCS::Internals.lcs(hello_ary, hello_ary)).to \ 30 | eq((0...hello_ary.size).to_a) 31 | end 32 | end 33 | 34 | describe Diff::LCS, ".LCS" do 35 | include Diff::LCS::SpecHelper::Matchers 36 | 37 | it "returns the correct compacted values from Diff::LCS.LCS" do 38 | res = Diff::LCS.LCS(seq1, seq2) 39 | expect(res).to eq(correct_lcs) 40 | expect(res.compact).to eq(res) 41 | end 42 | 43 | it "is transitive" do 44 | res = Diff::LCS.LCS(seq2, seq1) 45 | expect(res).to eq(correct_lcs) 46 | expect(res.compact).to eq(res) 47 | end 48 | 49 | it "returns %W(h e l l o) with (hello, hello)" do 50 | expect(Diff::LCS.LCS(hello, hello)).to eq(hello.chars) 51 | end 52 | 53 | it "returns hello_ary with (hello_ary, hello_ary)" do 54 | expect(Diff::LCS.LCS(hello_ary, hello_ary)).to eq(hello_ary) 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/ldiff_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "bin/ldiff" do 6 | include CaptureSubprocessIO 7 | 8 | # standard:disable Style/HashSyntax 9 | fixtures = [ 10 | {:name => "diff", :left => "aX", :right => "bXaX", :diff => 1}, 11 | {:name => "diff.missing_new_line1", :left => "four_lines", :right => "four_lines_with_missing_new_line", :diff => 1}, 12 | {:name => "diff.missing_new_line2", :left => "four_lines_with_missing_new_line", :right => "four_lines", :diff => 1}, 13 | {:name => "diff.issue95_trailing_context", :left => "123_x", :right => "456_x", :diff => 1}, 14 | {:name => "diff.four_lines.vs.empty", :left => "four_lines", :right => "empty", :diff => 1}, 15 | {:name => "diff.empty.vs.four_lines", :left => "empty", :right => "four_lines", :diff => 1}, 16 | {:name => "diff.bin1", :left => "file1.bin", :right => "file1.bin", :diff => 0}, 17 | {:name => "diff.bin2", :left => "file1.bin", :right => "file2.bin", :diff => 1}, 18 | {:name => "diff.chef", :left => "old-chef", :right => "new-chef", :diff => 1}, 19 | {:name => "diff.chef2", :left => "old-chef2", :right => "new-chef2", :diff => 1} 20 | ].product([nil, "-e", "-f", "-c", "-u"]).map { |(fixture, flag)| 21 | fixture = fixture.dup 22 | fixture[:flag] = flag 23 | fixture 24 | } 25 | # standard:enable Style/HashSyntax 26 | 27 | def self.test_ldiff(fixture) 28 | desc = [ 29 | fixture[:flag], 30 | "spec/fixtures/#{fixture[:left]}", 31 | "spec/fixtures/#{fixture[:right]}", 32 | "#", 33 | "=>", 34 | "spec/fixtures/ldiff/output.#{fixture[:name]}#{fixture[:flag]}" 35 | ].join(" ") 36 | 37 | it desc do 38 | stdout, stderr, status = run_ldiff(fixture) 39 | expect(status).to eq(fixture[:diff]) 40 | expect(stderr).to eq(read_fixture(fixture, mode: "error", allow_missing: true)) 41 | expect(stdout).to eq(read_fixture(fixture, mode: "output", allow_missing: false)) 42 | end 43 | end 44 | 45 | fixtures.each do |fixture| 46 | test_ldiff(fixture) 47 | end 48 | 49 | def read_fixture(options, mode: "output", allow_missing: false) 50 | fixture = options.fetch(:name) 51 | flag = options.fetch(:flag) 52 | name = "spec/fixtures/ldiff/#{mode}.#{fixture}#{flag}" 53 | 54 | return "" if !::File.exist?(name) && allow_missing 55 | 56 | data = IO.__send__(IO.respond_to?(:binread) ? :binread : :read, name) 57 | clean_data(data, flag) 58 | end 59 | 60 | def clean_data(data, flag) 61 | data = 62 | case flag 63 | when "-c", "-u" 64 | clean_output_timestamp(data) 65 | else 66 | data 67 | end 68 | data.gsub(/\r\n?/, "\n") 69 | end 70 | 71 | def clean_output_timestamp(data) 72 | data.gsub( 73 | %r{ 74 | ^ 75 | [-+*]{3} 76 | \s* 77 | spec/fixtures/(\S+) 78 | \s* 79 | \d{4}-\d\d-\d\d 80 | \s* 81 | \d\d:\d\d:\d\d(?:\.\d+) 82 | \s* 83 | (?:[-+]\d{4}|Z) 84 | }x, 85 | '*** spec/fixtures/\1 0000-00-00 :00 =>:00 =>00.000000000 -0000' 86 | ) 87 | end 88 | 89 | def run_ldiff(options) 90 | flag = options.fetch(:flag) 91 | left = options.fetch(:left) 92 | right = options.fetch(:right) 93 | 94 | stdout, stderr = capture_subprocess_io do 95 | system("ruby -Ilib bin/ldiff #{flag} spec/fixtures/#{left} spec/fixtures/#{right}") 96 | end 97 | 98 | [clean_data(stdout, flag), stderr, $?.exitstatus] 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/patch_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe "Diff::LCS.patch" do 6 | include Diff::LCS::SpecHelper::Matchers 7 | 8 | shared_examples "patch sequences correctly" do 9 | it "correctly patches left-to-right (patch autodiscovery)" do 10 | expect(Diff::LCS.patch(s1, patch_set)).to eq(s2) 11 | end 12 | 13 | it "correctly patches left-to-right (explicit patch)" do 14 | expect(Diff::LCS.patch(s1, patch_set, :patch)).to eq(s2) 15 | expect(Diff::LCS.patch!(s1, patch_set)).to eq(s2) 16 | end 17 | 18 | it "correctly patches right-to-left (unpatch autodiscovery)" do 19 | expect(Diff::LCS.patch(s2, patch_set)).to eq(s1) 20 | end 21 | 22 | it "correctly patches right-to-left (explicit unpatch)" do 23 | expect(Diff::LCS.patch(s2, patch_set, :unpatch)).to eq(s1) 24 | expect(Diff::LCS.unpatch!(s2, patch_set)).to eq(s1) 25 | end 26 | end 27 | 28 | describe "using a Diff::LCS.diff patchset" do 29 | describe "an empty patchset returns the source" do 30 | it "works on a string (hello)" do 31 | diff = Diff::LCS.diff(hello, hello) 32 | expect(Diff::LCS.patch(hello, diff)).to eq(hello) 33 | end 34 | 35 | it "works on an array %W(h e l l o)" do 36 | diff = Diff::LCS.diff(hello_ary, hello_ary) 37 | expect(Diff::LCS.patch(hello_ary, diff)).to eq(hello_ary) 38 | end 39 | end 40 | 41 | describe "with default diff callbacks (DiffCallbacks)" do 42 | describe "forward (s1 -> s2)" do 43 | it_has_behavior "patch sequences correctly" do 44 | let(:s1) { seq1 } 45 | let(:s2) { seq2 } 46 | let(:patch_set) { Diff::LCS.diff(seq1, seq2) } 47 | end 48 | end 49 | 50 | describe "reverse (s2 -> s1)" do 51 | it_has_behavior "patch sequences correctly" do 52 | let(:s1) { seq2 } 53 | let(:s2) { seq1 } 54 | let(:patch_set) { Diff::LCS.diff(seq2, seq1) } 55 | end 56 | end 57 | end 58 | 59 | describe "with context diff callbacks (ContextDiffCallbacks)" do 60 | describe "forward (s1 -> s2)" do 61 | it_has_behavior "patch sequences correctly" do 62 | let(:s1) { seq1 } 63 | let(:s2) { seq2 } 64 | let(:patch_set) { 65 | Diff::LCS.diff(seq1, seq2, Diff::LCS::ContextDiffCallbacks) 66 | } 67 | end 68 | end 69 | 70 | describe "reverse (s2 -> s1)" do 71 | it_has_behavior "patch sequences correctly" do 72 | let(:s1) { seq2 } 73 | let(:s2) { seq1 } 74 | let(:patch_set) { 75 | Diff::LCS.diff(seq2, seq1, Diff::LCS::ContextDiffCallbacks) 76 | } 77 | end 78 | end 79 | end 80 | 81 | describe "with sdiff callbacks (SDiffCallbacks)" do 82 | describe "forward (s1 -> s2)" do 83 | it_has_behavior "patch sequences correctly" do 84 | let(:s1) { seq1 } 85 | let(:s2) { seq2 } 86 | let(:patch_set) { 87 | Diff::LCS.diff(seq1, seq2, Diff::LCS::SDiffCallbacks) 88 | } 89 | end 90 | end 91 | 92 | describe "reverse (s2 -> s1)" do 93 | it_has_behavior "patch sequences correctly" do 94 | let(:s1) { seq2 } 95 | let(:s2) { seq1 } 96 | let(:patch_set) { 97 | Diff::LCS.diff(seq2, seq1, Diff::LCS::SDiffCallbacks) 98 | } 99 | end 100 | end 101 | end 102 | end 103 | 104 | describe "using a Diff::LCS.sdiff patchset" do 105 | describe "an empty patchset returns the source" do 106 | it "works on a string (hello)" do 107 | expect(Diff::LCS.patch(hello, Diff::LCS.sdiff(hello, hello))).to eq(hello) 108 | end 109 | 110 | it "works on an array %W(h e l l o)" do 111 | expect(Diff::LCS.patch(hello_ary, Diff::LCS.sdiff(hello_ary, hello_ary))).to eq(hello_ary) 112 | end 113 | end 114 | 115 | describe "with default diff callbacks (DiffCallbacks)" do 116 | describe "forward (s1 -> s2)" do 117 | it_has_behavior "patch sequences correctly" do 118 | let(:s1) { seq1 } 119 | let(:s2) { seq2 } 120 | let(:patch_set) { 121 | Diff::LCS.sdiff(seq1, seq2, Diff::LCS::DiffCallbacks) 122 | } 123 | end 124 | end 125 | 126 | describe "reverse (s2 -> s1)" do 127 | it_has_behavior "patch sequences correctly" do 128 | let(:s1) { seq2 } 129 | let(:s2) { seq1 } 130 | let(:patch_set) { 131 | Diff::LCS.sdiff(seq2, seq1, Diff::LCS::DiffCallbacks) 132 | } 133 | end 134 | end 135 | end 136 | 137 | describe "with context diff callbacks (DiffCallbacks)" do 138 | describe "forward (s1 -> s2)" do 139 | it_has_behavior "patch sequences correctly" do 140 | let(:s1) { seq1 } 141 | let(:s2) { seq2 } 142 | let(:patch_set) { 143 | Diff::LCS.sdiff(seq1, seq2, Diff::LCS::ContextDiffCallbacks) 144 | } 145 | end 146 | end 147 | 148 | describe "reverse (s2 -> s1)" do 149 | it_has_behavior "patch sequences correctly" do 150 | let(:s1) { seq2 } 151 | let(:s2) { seq1 } 152 | let(:patch_set) { 153 | Diff::LCS.sdiff(seq2, seq1, Diff::LCS::ContextDiffCallbacks) 154 | } 155 | end 156 | end 157 | end 158 | 159 | describe "with sdiff callbacks (SDiffCallbacks)" do 160 | describe "forward (s1 -> s2)" do 161 | it_has_behavior "patch sequences correctly" do 162 | let(:s1) { seq1 } 163 | let(:s2) { seq2 } 164 | let(:patch_set) { Diff::LCS.sdiff(seq1, seq2) } 165 | end 166 | end 167 | 168 | describe "reverse (s2 -> s1)" do 169 | it_has_behavior "patch sequences correctly" do 170 | let(:s1) { seq2 } 171 | let(:s2) { seq1 } 172 | let(:patch_set) { Diff::LCS.sdiff(seq2, seq1) } 173 | end 174 | end 175 | end 176 | end 177 | 178 | # Note: because of the error in autodiscovery ("does not autodiscover s1 179 | # to s2 patches"), this cannot use the "patch sequences correctly" shared 180 | # set. Once the bug in autodiscovery is fixed, this can be converted as 181 | # above. 182 | describe "fix bug 891: patchsets do not contain the last equal part" do 183 | before :each do 184 | @s1 = %w[a b c d e f g h i j k] # standard:disable Layout/SpaceInsideArrayPercentLiteral 185 | @s2 = %w[a b c d D e f g h i j k] 186 | end 187 | 188 | describe "using Diff::LCS.diff with default diff callbacks" do 189 | before :each do 190 | @patch_set_s1_s2 = Diff::LCS.diff(@s1, @s2) 191 | @patch_set_s2_s1 = Diff::LCS.diff(@s2, @s1) 192 | end 193 | 194 | it "autodiscovers s1 to s2 patches" do 195 | expect do 196 | expect(Diff::LCS.patch(@s1, @patch_set_s1_s2)).to eq(@s2) 197 | end.to_not raise_error 198 | end 199 | 200 | it "autodiscovers s2 to s1 patches" do 201 | expect do 202 | expect(Diff::LCS.patch(@s1, @patch_set_s2_s1)).to eq(@s2) 203 | end.to_not raise_error 204 | end 205 | 206 | it "autodiscovers s2 to s1 the left-to-right patches" do 207 | expect(Diff::LCS.patch(@s2, @patch_set_s2_s1)).to eq(@s1) 208 | expect(Diff::LCS.patch(@s2, @patch_set_s1_s2)).to eq(@s1) 209 | end 210 | 211 | it "correctly patches left-to-right (explicit patch)" do 212 | expect(Diff::LCS.patch(@s1, @patch_set_s1_s2, :patch)).to eq(@s2) 213 | expect(Diff::LCS.patch(@s2, @patch_set_s2_s1, :patch)).to eq(@s1) 214 | expect(Diff::LCS.patch!(@s1, @patch_set_s1_s2)).to eq(@s2) 215 | expect(Diff::LCS.patch!(@s2, @patch_set_s2_s1)).to eq(@s1) 216 | end 217 | 218 | it "correctly patches right-to-left (explicit unpatch)" do 219 | expect(Diff::LCS.patch(@s2, @patch_set_s1_s2, :unpatch)).to eq(@s1) 220 | expect(Diff::LCS.patch(@s1, @patch_set_s2_s1, :unpatch)).to eq(@s2) 221 | expect(Diff::LCS.unpatch!(@s2, @patch_set_s1_s2)).to eq(@s1) 222 | expect(Diff::LCS.unpatch!(@s1, @patch_set_s2_s1)).to eq(@s2) 223 | end 224 | end 225 | 226 | describe "using Diff::LCS.diff with context diff callbacks" do 227 | before :each do 228 | @patch_set_s1_s2 = Diff::LCS.diff(@s1, @s2, Diff::LCS::ContextDiffCallbacks) 229 | @patch_set_s2_s1 = Diff::LCS.diff(@s2, @s1, Diff::LCS::ContextDiffCallbacks) 230 | end 231 | 232 | it "autodiscovers s1 to s2 patches" do 233 | expect do 234 | expect(Diff::LCS.patch(@s1, @patch_set_s1_s2)).to eq(@s2) 235 | end.to_not raise_error 236 | end 237 | 238 | it "autodiscovers s2 to s1 patches" do 239 | expect do 240 | expect(Diff::LCS.patch(@s1, @patch_set_s2_s1)).to eq(@s2) 241 | end.to_not raise_error 242 | end 243 | 244 | it "autodiscovers s2 to s1 the left-to-right patches" do 245 | expect(Diff::LCS.patch(@s2, @patch_set_s2_s1)).to eq(@s1) 246 | expect(Diff::LCS.patch(@s2, @patch_set_s1_s2)).to eq(@s1) 247 | end 248 | 249 | it "correctly patches left-to-right (explicit patch)" do 250 | expect(Diff::LCS.patch(@s1, @patch_set_s1_s2, :patch)).to eq(@s2) 251 | expect(Diff::LCS.patch(@s2, @patch_set_s2_s1, :patch)).to eq(@s1) 252 | expect(Diff::LCS.patch!(@s1, @patch_set_s1_s2)).to eq(@s2) 253 | expect(Diff::LCS.patch!(@s2, @patch_set_s2_s1)).to eq(@s1) 254 | end 255 | 256 | it "correctly patches right-to-left (explicit unpatch)" do 257 | expect(Diff::LCS.patch(@s2, @patch_set_s1_s2, :unpatch)).to eq(@s1) 258 | expect(Diff::LCS.patch(@s1, @patch_set_s2_s1, :unpatch)).to eq(@s2) 259 | expect(Diff::LCS.unpatch!(@s2, @patch_set_s1_s2)).to eq(@s1) 260 | expect(Diff::LCS.unpatch!(@s1, @patch_set_s2_s1)).to eq(@s2) 261 | end 262 | end 263 | 264 | describe "using Diff::LCS.diff with sdiff callbacks" do 265 | before(:each) do 266 | @patch_set_s1_s2 = Diff::LCS.diff(@s1, @s2, Diff::LCS::SDiffCallbacks) 267 | @patch_set_s2_s1 = Diff::LCS.diff(@s2, @s1, Diff::LCS::SDiffCallbacks) 268 | end 269 | 270 | it "autodiscovers s1 to s2 patches" do 271 | expect do 272 | expect(Diff::LCS.patch(@s1, @patch_set_s1_s2)).to eq(@s2) 273 | end.to_not raise_error 274 | end 275 | 276 | it "autodiscovers s2 to s1 patches" do 277 | expect do 278 | expect(Diff::LCS.patch(@s1, @patch_set_s2_s1)).to eq(@s2) 279 | end.to_not raise_error 280 | end 281 | 282 | it "autodiscovers s2 to s1 the left-to-right patches" do 283 | expect(Diff::LCS.patch(@s2, @patch_set_s2_s1)).to eq(@s1) 284 | expect(Diff::LCS.patch(@s2, @patch_set_s1_s2)).to eq(@s1) 285 | end 286 | 287 | it "correctly patches left-to-right (explicit patch)" do 288 | expect(Diff::LCS.patch(@s1, @patch_set_s1_s2, :patch)).to eq(@s2) 289 | expect(Diff::LCS.patch(@s2, @patch_set_s2_s1, :patch)).to eq(@s1) 290 | expect(Diff::LCS.patch!(@s1, @patch_set_s1_s2)).to eq(@s2) 291 | expect(Diff::LCS.patch!(@s2, @patch_set_s2_s1)).to eq(@s1) 292 | end 293 | 294 | it "correctly patches right-to-left (explicit unpatch)" do 295 | expect(Diff::LCS.patch(@s2, @patch_set_s1_s2, :unpatch)).to eq(@s1) 296 | expect(Diff::LCS.patch(@s1, @patch_set_s2_s1, :unpatch)).to eq(@s2) 297 | expect(Diff::LCS.unpatch!(@s2, @patch_set_s1_s2)).to eq(@s1) 298 | expect(Diff::LCS.unpatch!(@s1, @patch_set_s2_s1)).to eq(@s2) 299 | end 300 | end 301 | 302 | describe "using Diff::LCS.sdiff with default sdiff callbacks" do 303 | before(:each) do 304 | @patch_set_s1_s2 = Diff::LCS.sdiff(@s1, @s2) 305 | @patch_set_s2_s1 = Diff::LCS.sdiff(@s2, @s1) 306 | end 307 | 308 | it "autodiscovers s1 to s2 patches" do 309 | expect do 310 | expect(Diff::LCS.patch(@s1, @patch_set_s1_s2)).to eq(@s2) 311 | end.to_not raise_error 312 | end 313 | 314 | it "autodiscovers s2 to s1 patches" do 315 | expect do 316 | expect(Diff::LCS.patch(@s1, @patch_set_s2_s1)).to eq(@s2) 317 | end.to_not raise_error 318 | end 319 | 320 | it "autodiscovers s2 to s1 the left-to-right patches" do 321 | expect(Diff::LCS.patch(@s2, @patch_set_s2_s1)).to eq(@s1) 322 | expect(Diff::LCS.patch(@s2, @patch_set_s1_s2)).to eq(@s1) 323 | end 324 | 325 | it "correctly patches left-to-right (explicit patch)" do 326 | expect(Diff::LCS.patch(@s1, @patch_set_s1_s2, :patch)).to eq(@s2) 327 | expect(Diff::LCS.patch(@s2, @patch_set_s2_s1, :patch)).to eq(@s1) 328 | expect(Diff::LCS.patch!(@s1, @patch_set_s1_s2)).to eq(@s2) 329 | expect(Diff::LCS.patch!(@s2, @patch_set_s2_s1)).to eq(@s1) 330 | end 331 | 332 | it "correctly patches right-to-left (explicit unpatch)" do 333 | expect(Diff::LCS.patch(@s2, @patch_set_s1_s2, :unpatch)).to eq(@s1) 334 | expect(Diff::LCS.patch(@s1, @patch_set_s2_s1, :unpatch)).to eq(@s2) 335 | expect(Diff::LCS.unpatch!(@s2, @patch_set_s1_s2)).to eq(@s1) 336 | expect(Diff::LCS.unpatch!(@s1, @patch_set_s2_s1)).to eq(@s2) 337 | end 338 | end 339 | 340 | describe "using Diff::LCS.sdiff with context diff callbacks" do 341 | before(:each) do 342 | @patch_set_s1_s2 = Diff::LCS.sdiff(@s1, @s2, Diff::LCS::ContextDiffCallbacks) 343 | @patch_set_s2_s1 = Diff::LCS.sdiff(@s2, @s1, Diff::LCS::ContextDiffCallbacks) 344 | end 345 | 346 | it "autodiscovers s1 to s2 patches" do 347 | expect do 348 | expect(Diff::LCS.patch(@s1, @patch_set_s1_s2)).to eq(@s2) 349 | end.to_not raise_error 350 | end 351 | 352 | it "autodiscovers s2 to s1 patches" do 353 | expect do 354 | expect(Diff::LCS.patch(@s1, @patch_set_s2_s1)).to eq(@s2) 355 | end.to_not raise_error 356 | end 357 | 358 | it "autodiscovers s2 to s1 the left-to-right patches" do 359 | expect(Diff::LCS.patch(@s2, @patch_set_s2_s1)).to eq(@s1) 360 | expect(Diff::LCS.patch(@s2, @patch_set_s1_s2)).to eq(@s1) 361 | end 362 | 363 | it "correctly patches left-to-right (explicit patch)" do 364 | expect(Diff::LCS.patch(@s1, @patch_set_s1_s2, :patch)).to eq(@s2) 365 | expect(Diff::LCS.patch(@s2, @patch_set_s2_s1, :patch)).to eq(@s1) 366 | expect(Diff::LCS.patch!(@s1, @patch_set_s1_s2)).to eq(@s2) 367 | expect(Diff::LCS.patch!(@s2, @patch_set_s2_s1)).to eq(@s1) 368 | end 369 | 370 | it "correctly patches right-to-left (explicit unpatch)" do 371 | expect(Diff::LCS.patch(@s2, @patch_set_s1_s2, :unpatch)).to eq(@s1) 372 | expect(Diff::LCS.patch(@s1, @patch_set_s2_s1, :unpatch)).to eq(@s2) 373 | expect(Diff::LCS.unpatch!(@s2, @patch_set_s1_s2)).to eq(@s1) 374 | expect(Diff::LCS.unpatch!(@s1, @patch_set_s2_s1)).to eq(@s2) 375 | end 376 | end 377 | 378 | describe "using Diff::LCS.sdiff with default diff callbacks" do 379 | before(:each) do 380 | @patch_set_s1_s2 = Diff::LCS.sdiff(@s1, @s2, Diff::LCS::DiffCallbacks) 381 | @patch_set_s2_s1 = Diff::LCS.sdiff(@s2, @s1, Diff::LCS::DiffCallbacks) 382 | end 383 | 384 | it "autodiscovers s1 to s2 patches" do 385 | expect do 386 | expect(Diff::LCS.patch(@s1, @patch_set_s1_s2)).to eq(@s2) 387 | end.to_not raise_error 388 | end 389 | 390 | it "autodiscovers s2 to s1 patches" do 391 | expect do 392 | expect(Diff::LCS.patch(@s1, @patch_set_s2_s1)).to eq(@s2) 393 | end.to_not raise_error 394 | end 395 | 396 | it "autodiscovers s2 to s1 the left-to-right patches" do 397 | expect(Diff::LCS.patch(@s2, @patch_set_s2_s1)).to eq(@s1) 398 | expect(Diff::LCS.patch(@s2, @patch_set_s1_s2)).to eq(@s1) 399 | end 400 | 401 | it "correctly patches left-to-right (explicit patch)" do 402 | expect(Diff::LCS.patch(@s1, @patch_set_s1_s2, :patch)).to eq(@s2) 403 | expect(Diff::LCS.patch(@s2, @patch_set_s2_s1, :patch)).to eq(@s1) 404 | expect(Diff::LCS.patch!(@s1, @patch_set_s1_s2)).to eq(@s2) 405 | expect(Diff::LCS.patch!(@s2, @patch_set_s2_s1)).to eq(@s1) 406 | end 407 | 408 | it "correctly patches right-to-left (explicit unpatch)" do 409 | expect(Diff::LCS.patch(@s2, @patch_set_s1_s2, :unpatch)).to eq(@s1) 410 | expect(Diff::LCS.patch(@s1, @patch_set_s2_s1, :unpatch)).to eq(@s2) 411 | expect(Diff::LCS.unpatch!(@s2, @patch_set_s1_s2)).to eq(@s1) 412 | expect(Diff::LCS.unpatch!(@s1, @patch_set_s2_s1)).to eq(@s2) 413 | end 414 | end 415 | end 416 | end 417 | -------------------------------------------------------------------------------- /spec/sdiff_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe "Diff::LCS.sdiff" do 6 | include Diff::LCS::SpecHelper::Matchers 7 | 8 | shared_examples "compare sequences correctly" do 9 | it "compares s1 -> s2 correctly" do 10 | expect(Diff::LCS.sdiff(s1, s2)).to eq(context_diff(result)) 11 | end 12 | 13 | it "compares s2 -> s1 correctly" do 14 | expect(Diff::LCS.sdiff(s2, s1)).to eq(context_diff(reverse_sdiff(result))) 15 | end 16 | end 17 | 18 | describe "using seq1 & seq2" do 19 | let(:s1) { seq1 } 20 | let(:s2) { seq2 } 21 | let(:result) { correct_forward_sdiff } 22 | 23 | it_has_behavior "compare sequences correctly" 24 | end 25 | 26 | describe "using %w(abc def yyy xxx ghi jkl) & %w(abc dxf xxx ghi jkl)" do 27 | let(:s1) { %w[abc def yyy xxx ghi jkl] } 28 | let(:s2) { %w[abc dxf xxx ghi jkl] } 29 | let(:result) { 30 | # standard:disable Layout/ExtraSpacing 31 | [ 32 | ["=", [0, "abc"], [0, "abc"]], 33 | ["!", [1, "def"], [1, "dxf"]], 34 | ["-", [2, "yyy"], [2, nil]], 35 | ["=", [3, "xxx"], [2, "xxx"]], 36 | ["=", [4, "ghi"], [3, "ghi"]], 37 | ["=", [5, "jkl"], [4, "jkl"]] 38 | ] 39 | # standard:enable Layout/ExtraSpacing 40 | } 41 | 42 | it_has_behavior "compare sequences correctly" 43 | end 44 | 45 | describe "using %w(a b c d e) & %w(a e)" do 46 | let(:s1) { %w[a b c d e] } 47 | let(:s2) { %w[a e] } 48 | let(:result) { 49 | [ 50 | ["=", [0, "a"], [0, "a"]], 51 | ["-", [1, "b"], [1, nil]], 52 | ["-", [2, "c"], [1, nil]], 53 | ["-", [3, "d"], [1, nil]], 54 | ["=", [4, "e"], [1, "e"]] 55 | ] 56 | } 57 | 58 | it_has_behavior "compare sequences correctly" 59 | end 60 | 61 | describe "using %w(a e) & %w(a b c d e)" do 62 | let(:s1) { %w[a e] } 63 | let(:s2) { %w[a b c d e] } 64 | let(:result) { 65 | [ 66 | ["=", [0, "a"], [0, "a"]], 67 | ["+", [1, nil], [1, "b"]], 68 | ["+", [1, nil], [2, "c"]], 69 | ["+", [1, nil], [3, "d"]], 70 | ["=", [1, "e"], [4, "e"]] 71 | ] 72 | } 73 | 74 | it_has_behavior "compare sequences correctly" 75 | end 76 | 77 | describe "using %w(v x a e) & %w(w y a b c d e)" do 78 | let(:s1) { %w[v x a e] } 79 | let(:s2) { %w[w y a b c d e] } 80 | let(:result) { 81 | [ 82 | ["!", [0, "v"], [0, "w"]], 83 | ["!", [1, "x"], [1, "y"]], 84 | ["=", [2, "a"], [2, "a"]], 85 | ["+", [3, nil], [3, "b"]], 86 | ["+", [3, nil], [4, "c"]], 87 | ["+", [3, nil], [5, "d"]], 88 | ["=", [3, "e"], [6, "e"]] 89 | ] 90 | } 91 | 92 | it_has_behavior "compare sequences correctly" 93 | end 94 | 95 | describe "using %w(x a e) & %w(a b c d e)" do 96 | let(:s1) { %w[x a e] } 97 | let(:s2) { %w[a b c d e] } 98 | let(:result) { 99 | [ 100 | ["-", [0, "x"], [0, nil]], 101 | ["=", [1, "a"], [0, "a"]], 102 | ["+", [2, nil], [1, "b"]], 103 | ["+", [2, nil], [2, "c"]], 104 | ["+", [2, nil], [3, "d"]], 105 | ["=", [2, "e"], [4, "e"]] 106 | ] 107 | } 108 | 109 | it_has_behavior "compare sequences correctly" 110 | end 111 | 112 | describe "using %w(a e) & %w(x a b c d e)" do 113 | let(:s1) { %w[a e] } 114 | let(:s2) { %w[x a b c d e] } 115 | let(:result) { 116 | [ 117 | ["+", [0, nil], [0, "x"]], 118 | ["=", [0, "a"], [1, "a"]], 119 | ["+", [1, nil], [2, "b"]], 120 | ["+", [1, nil], [3, "c"]], 121 | ["+", [1, nil], [4, "d"]], 122 | ["=", [1, "e"], [5, "e"]] 123 | ] 124 | } 125 | 126 | it_has_behavior "compare sequences correctly" 127 | end 128 | 129 | describe "using %w(a e v) & %w(x a b c d e w x)" do 130 | let(:s1) { %w[a e v] } 131 | let(:s2) { %w[x a b c d e w x] } 132 | let(:result) { 133 | [ 134 | ["+", [0, nil], [0, "x"]], 135 | ["=", [0, "a"], [1, "a"]], 136 | ["+", [1, nil], [2, "b"]], 137 | ["+", [1, nil], [3, "c"]], 138 | ["+", [1, nil], [4, "d"]], 139 | ["=", [1, "e"], [5, "e"]], 140 | ["!", [2, "v"], [6, "w"]], 141 | ["+", [3, nil], [7, "x"]] 142 | ] 143 | } 144 | 145 | it_has_behavior "compare sequences correctly" 146 | end 147 | 148 | describe "using %w() & %w(a b c)" do 149 | let(:s1) { %w[] } 150 | let(:s2) { %w[a b c] } 151 | let(:result) { 152 | [ 153 | ["+", [0, nil], [0, "a"]], 154 | ["+", [0, nil], [1, "b"]], 155 | ["+", [0, nil], [2, "c"]] 156 | ] 157 | } 158 | 159 | it_has_behavior "compare sequences correctly" 160 | end 161 | 162 | describe "using %w(a b c) & %w(1)" do 163 | let(:s1) { %w[a b c] } 164 | let(:s2) { %w[1] } 165 | let(:result) { 166 | [ 167 | ["!", [0, "a"], [0, "1"]], 168 | ["-", [1, "b"], [1, nil]], 169 | ["-", [2, "c"], [1, nil]] 170 | ] 171 | } 172 | 173 | it_has_behavior "compare sequences correctly" 174 | end 175 | 176 | describe "using %w(a b c) & %w(c)" do 177 | let(:s1) { %w[a b c] } 178 | let(:s2) { %w[c] } 179 | let(:result) { 180 | [ 181 | ["-", [0, "a"], [0, nil]], 182 | ["-", [1, "b"], [0, nil]], 183 | ["=", [2, "c"], [0, "c"]] 184 | ] 185 | } 186 | 187 | it_has_behavior "compare sequences correctly" 188 | end 189 | 190 | describe "using %w(abcd efgh ijkl mnop) & []" do 191 | let(:s1) { %w[abcd efgh ijkl mnop] } 192 | let(:s2) { [] } 193 | let(:result) { 194 | [ 195 | ["-", [0, "abcd"], [0, nil]], 196 | ["-", [1, "efgh"], [0, nil]], 197 | ["-", [2, "ijkl"], [0, nil]], 198 | ["-", [3, "mnop"], [0, nil]] 199 | ] 200 | } 201 | 202 | it_has_behavior "compare sequences correctly" 203 | end 204 | 205 | describe "using [[1,2]] & []" do 206 | let(:s1) { [[1, 2]] } 207 | let(:s2) { [] } 208 | let(:result) { 209 | [ 210 | ["-", [0, [1, 2]], [0, nil]] 211 | ] 212 | } 213 | 214 | it_has_behavior "compare sequences correctly" 215 | end 216 | end 217 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rubygems" 4 | require "pathname" 5 | 6 | require "psych" if RUBY_VERSION >= "1.9" 7 | 8 | if ENV["COVERAGE"] 9 | require "simplecov" 10 | require "simplecov-lcov" 11 | 12 | SimpleCov::Formatter::LcovFormatter.config do |config| 13 | config.report_with_single_file = true 14 | config.lcov_file_name = "lcov.info" 15 | end 16 | 17 | SimpleCov.start "test_frameworks" do 18 | enable_coverage :branch 19 | primary_coverage :branch 20 | formatter SimpleCov::Formatter::MultiFormatter.new([ 21 | SimpleCov::Formatter::HTMLFormatter, 22 | SimpleCov::Formatter::LcovFormatter, 23 | SimpleCov::Formatter::SimpleFormatter 24 | ]) 25 | end 26 | end 27 | 28 | file = Pathname.new(__FILE__).expand_path 29 | path = file.parent 30 | parent = path.parent 31 | 32 | $:.unshift parent.join("lib") 33 | 34 | module CaptureSubprocessIO 35 | def _synchronize 36 | yield 37 | end 38 | 39 | def capture_subprocess_io 40 | _synchronize { _capture_subprocess_io { yield } } 41 | end 42 | 43 | def _capture_subprocess_io 44 | require "tempfile" 45 | 46 | captured_stdout, captured_stderr = Tempfile.new("out"), Tempfile.new("err") 47 | 48 | orig_stdout, orig_stderr = $stdout.dup, $stderr.dup 49 | $stdout.reopen captured_stdout 50 | $stderr.reopen captured_stderr 51 | 52 | yield 53 | 54 | $stdout.rewind 55 | $stderr.rewind 56 | 57 | [captured_stdout.read, captured_stderr.read] 58 | ensure 59 | captured_stdout.unlink 60 | captured_stderr.unlink 61 | $stdout.reopen orig_stdout 62 | $stderr.reopen orig_stderr 63 | end 64 | private :_capture_subprocess_io 65 | end 66 | 67 | require "diff-lcs" 68 | 69 | module Diff::LCS::SpecHelper 70 | def hello 71 | "hello" 72 | end 73 | 74 | def hello_ary 75 | %w[h e l l o] 76 | end 77 | 78 | def seq1 79 | %w[a b c e h j l m n p] 80 | end 81 | 82 | def skipped_seq1 83 | %w[a h n p] 84 | end 85 | 86 | def seq2 87 | %w[b c d e f j k l m r s t] 88 | end 89 | 90 | def skipped_seq2 91 | %w[d f k r s t] 92 | end 93 | 94 | def word_sequence 95 | %w[abcd efgh ijkl mnopqrstuvwxyz] 96 | end 97 | 98 | def correct_lcs 99 | %w[b c e j l m] 100 | end 101 | 102 | # standard:disable Layout/ExtraSpacing 103 | def correct_forward_diff 104 | [ 105 | [ 106 | ["-", 0, "a"] 107 | ], 108 | [ 109 | ["+", 2, "d"] 110 | ], 111 | [ 112 | ["-", 4, "h"], 113 | ["+", 4, "f"] 114 | ], 115 | [ 116 | ["+", 6, "k"] 117 | ], 118 | [ 119 | ["-", 8, "n"], 120 | ["+", 9, "r"], 121 | ["-", 9, "p"], 122 | ["+", 10, "s"], 123 | ["+", 11, "t"] 124 | ] 125 | ] 126 | end 127 | 128 | def correct_backward_diff 129 | [ 130 | [ 131 | ["+", 0, "a"] 132 | ], 133 | [ 134 | ["-", 2, "d"] 135 | ], 136 | [ 137 | ["-", 4, "f"], 138 | ["+", 4, "h"] 139 | ], 140 | [ 141 | ["-", 6, "k"] 142 | ], 143 | [ 144 | ["-", 9, "r"], 145 | ["+", 8, "n"], 146 | ["-", 10, "s"], 147 | ["+", 9, "p"], 148 | ["-", 11, "t"] 149 | ] 150 | ] 151 | end 152 | 153 | def correct_forward_sdiff 154 | [ 155 | ["-", [0, "a"], [0, nil]], 156 | ["=", [1, "b"], [0, "b"]], 157 | ["=", [2, "c"], [1, "c"]], 158 | ["+", [3, nil], [2, "d"]], 159 | ["=", [3, "e"], [3, "e"]], 160 | ["!", [4, "h"], [4, "f"]], 161 | ["=", [5, "j"], [5, "j"]], 162 | ["+", [6, nil], [6, "k"]], 163 | ["=", [6, "l"], [7, "l"]], 164 | ["=", [7, "m"], [8, "m"]], 165 | ["!", [8, "n"], [9, "r"]], 166 | ["!", [9, "p"], [10, "s"]], 167 | ["+", [10, nil], [11, "t"]] 168 | ] 169 | end 170 | # standard:enable Layout/ExtraSpacing 171 | 172 | def reverse_sdiff(forward_sdiff) 173 | forward_sdiff.map { |line| 174 | line[1], line[2] = line[2], line[1] 175 | case line[0] 176 | when "-" then line[0] = "+" 177 | when "+" then line[0] = "-" 178 | end 179 | line 180 | } 181 | end 182 | 183 | def change_diff(diff) 184 | map_diffs(diff, Diff::LCS::Change) 185 | end 186 | 187 | def context_diff(diff) 188 | map_diffs(diff, Diff::LCS::ContextChange) 189 | end 190 | 191 | def format_diffs(diffs) 192 | diffs.map { |e| 193 | if e.is_a?(Array) 194 | e.map { |f| f.to_a.join }.join(", ") 195 | else 196 | e.to_a.join 197 | end 198 | }.join("\n") 199 | end 200 | 201 | def map_diffs(diffs, klass = Diff::LCS::ContextChange) 202 | diffs.map do |chunks| 203 | if klass == Diff::LCS::ContextChange 204 | klass.from_a(chunks) 205 | else 206 | chunks.map { |changes| klass.from_a(changes) } 207 | end 208 | end 209 | end 210 | 211 | def balanced_traversal(s1, s2, callback_type) 212 | callback = __send__(callback_type) 213 | Diff::LCS.traverse_balanced(s1, s2, callback) 214 | callback 215 | end 216 | 217 | def balanced_reverse(change_result) 218 | new_result = [] 219 | change_result.each do |line| 220 | line = [line[0], line[2], line[1]] 221 | case line[0] 222 | when "<" 223 | line[0] = ">" 224 | when ">" 225 | line[0] = "<" 226 | end 227 | new_result << line 228 | end 229 | new_result.sort_by { |line| [line[1], line[2]] } 230 | end 231 | 232 | def map_to_no_change(change_result) 233 | new_result = [] 234 | change_result.each do |line| 235 | case line[0] 236 | when "!" 237 | new_result << ["<", line[1], line[2]] 238 | new_result << [">", line[1] + 1, line[2]] 239 | else 240 | new_result << line 241 | end 242 | end 243 | new_result 244 | end 245 | 246 | class SimpleCallback 247 | def initialize 248 | reset 249 | end 250 | 251 | attr_reader :matched_a 252 | attr_reader :matched_b 253 | attr_reader :discards_a 254 | attr_reader :discards_b 255 | attr_reader :done_a 256 | attr_reader :done_b 257 | 258 | def reset 259 | @matched_a = [] 260 | @matched_b = [] 261 | @discards_a = [] 262 | @discards_b = [] 263 | @done_a = [] 264 | @done_b = [] 265 | self 266 | end 267 | 268 | def match(event) 269 | @matched_a << event.old_element 270 | @matched_b << event.new_element 271 | end 272 | 273 | def discard_b(event) 274 | @discards_b << event.new_element 275 | end 276 | 277 | def discard_a(event) 278 | @discards_a << event.old_element 279 | end 280 | 281 | def finished_a(event) 282 | @done_a << [ 283 | event.old_element, event.old_position, 284 | event.new_element, event.new_position 285 | ] 286 | end 287 | 288 | def finished_b(event) 289 | @done_b << [ 290 | event.old_element, event.old_position, 291 | event.new_element, event.new_position 292 | ] 293 | end 294 | end 295 | 296 | def simple_callback 297 | SimpleCallback.new 298 | end 299 | 300 | class SimpleCallbackNoFinishers < SimpleCallback 301 | undef :finished_a 302 | undef :finished_b 303 | end 304 | 305 | def simple_callback_no_finishers 306 | SimpleCallbackNoFinishers.new 307 | end 308 | 309 | class BalancedCallback 310 | def initialize 311 | reset 312 | end 313 | 314 | attr_reader :result 315 | 316 | def reset 317 | @result = [] 318 | end 319 | 320 | def match(event) 321 | @result << ["=", event.old_position, event.new_position] 322 | end 323 | 324 | def discard_a(event) 325 | @result << ["<", event.old_position, event.new_position] 326 | end 327 | 328 | def discard_b(event) 329 | @result << [">", event.old_position, event.new_position] 330 | end 331 | 332 | def change(event) 333 | @result << ["!", event.old_position, event.new_position] 334 | end 335 | end 336 | 337 | def balanced_callback 338 | BalancedCallback.new 339 | end 340 | 341 | class BalancedCallbackNoChange < BalancedCallback 342 | undef :change 343 | end 344 | 345 | def balanced_callback_no_change 346 | BalancedCallbackNoChange.new 347 | end 348 | 349 | module Matchers 350 | extend RSpec::Matchers::DSL 351 | 352 | matcher :be_nil_or_match_values do |ii, s1, s2| 353 | match do |ee| 354 | expect(ee).to(satisfy { |vee| vee.nil? || s1[ii] == s2[ee] }) 355 | end 356 | end 357 | 358 | matcher :correctly_map_sequence do |s1| 359 | match do |actual| 360 | actual.each_index { |ii| expect(actual[ii]).to be_nil_or_match_values(ii, s1, @s2) } 361 | end 362 | 363 | chain :to_other_sequence do |s2| 364 | @s2 = s2 365 | end 366 | end 367 | end 368 | end 369 | 370 | RSpec.configure do |conf| 371 | conf.include Diff::LCS::SpecHelper 372 | conf.alias_it_should_behave_like_to :it_has_behavior, "has behavior:" 373 | # standard:disable Style/HashSyntax 374 | conf.filter_run_excluding :broken => true 375 | # standard:enable Style/HashSyntax 376 | end 377 | -------------------------------------------------------------------------------- /spec/traverse_balanced_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe "Diff::LCS.traverse_balanced" do 6 | include Diff::LCS::SpecHelper::Matchers 7 | 8 | shared_examples "with a #change callback" do |s1, s2, result| 9 | it "traverses s1 -> s2 correctly" do 10 | traversal = balanced_traversal(s1, s2, :balanced_callback) 11 | expect(traversal.result).to eq(result) 12 | end 13 | 14 | it "traverses s2 -> s1 correctly" do 15 | traversal = balanced_traversal(s2, s1, :balanced_callback) 16 | expect(traversal.result).to eq(balanced_reverse(result)) 17 | end 18 | end 19 | 20 | shared_examples "without a #change callback" do |s1, s2, result| 21 | it "traverses s1 -> s2 correctly" do 22 | traversal = balanced_traversal(s1, s2, :balanced_callback_no_change) 23 | expect(traversal.result).to eq(map_to_no_change(result)) 24 | end 25 | 26 | it "traverses s2 -> s1 correctly" do 27 | traversal = balanced_traversal(s2, s1, :balanced_callback_no_change) 28 | expect(traversal.result).to eq(map_to_no_change(balanced_reverse(result))) 29 | end 30 | end 31 | 32 | describe "identical string sequences ('abc')" do 33 | s1 = s2 = "abc" 34 | 35 | result = [ 36 | ["=", 0, 0], 37 | ["=", 1, 1], 38 | ["=", 2, 2] 39 | ] 40 | 41 | it_has_behavior "with a #change callback", s1, s2, result 42 | it_has_behavior "without a #change callback", s1, s2, result 43 | end 44 | 45 | describe "identical array sequences %w(a b c)" do 46 | s1 = s2 = %w[a b c] 47 | 48 | result = [ 49 | ["=", 0, 0], 50 | ["=", 1, 1], 51 | ["=", 2, 2] 52 | ] 53 | 54 | it_has_behavior "with a #change callback", s1, s2, result 55 | it_has_behavior "without a #change callback", s1, s2, result 56 | end 57 | 58 | describe "sequences %w(a b c) & %w(a x c)" do 59 | s1 = %w[a b c] 60 | s2 = %w[a x c] 61 | 62 | result = [ 63 | ["=", 0, 0], 64 | ["!", 1, 1], 65 | ["=", 2, 2] 66 | ] 67 | 68 | it_has_behavior "with a #change callback", s1, s2, result 69 | it_has_behavior "without a #change callback", s1, s2, result 70 | end 71 | 72 | describe "sequences %w(a x y c) & %w(a v w c)" do 73 | s1 = %w[a x y c] 74 | s2 = %w[a v w c] 75 | 76 | result = [ 77 | ["=", 0, 0], 78 | ["!", 1, 1], 79 | ["!", 2, 2], 80 | ["=", 3, 3] 81 | ] 82 | 83 | it_has_behavior "with a #change callback", s1, s2, result 84 | it_has_behavior "without a #change callback", s1, s2, result 85 | end 86 | 87 | describe "sequences %w(x y c) & %w(v w c)" do 88 | s1 = %w[x y c] 89 | s2 = %w[v w c] 90 | result = [ 91 | ["!", 0, 0], 92 | ["!", 1, 1], 93 | ["=", 2, 2] 94 | ] 95 | 96 | it_has_behavior "with a #change callback", s1, s2, result 97 | it_has_behavior "without a #change callback", s1, s2, result 98 | end 99 | 100 | describe "sequences %w(a x y z) & %w(b v w)" do 101 | s1 = %w[a x y z] 102 | s2 = %w[b v w] 103 | result = [ 104 | ["!", 0, 0], 105 | ["!", 1, 1], 106 | ["!", 2, 2], 107 | ["<", 3, 3] 108 | ] 109 | 110 | it_has_behavior "with a #change callback", s1, s2, result 111 | it_has_behavior "without a #change callback", s1, s2, result 112 | end 113 | 114 | describe "sequences %w(a z) & %w(a)" do 115 | s1 = %w[a z] 116 | s2 = %w[a] 117 | result = [ 118 | ["=", 0, 0], 119 | ["<", 1, 1] 120 | ] 121 | 122 | it_has_behavior "with a #change callback", s1, s2, result 123 | it_has_behavior "without a #change callback", s1, s2, result 124 | end 125 | 126 | describe "sequences %w(z a) & %w(a)" do 127 | s1 = %w[z a] 128 | s2 = %w[a] 129 | result = [ 130 | ["<", 0, 0], 131 | ["=", 1, 0] 132 | ] 133 | 134 | it_has_behavior "with a #change callback", s1, s2, result 135 | it_has_behavior "without a #change callback", s1, s2, result 136 | end 137 | 138 | describe "sequences %w(a b c) & %w(x y z)" do 139 | s1 = %w[a b c] 140 | s2 = %w[x y z] 141 | result = [ 142 | ["!", 0, 0], 143 | ["!", 1, 1], 144 | ["!", 2, 2] 145 | ] 146 | 147 | it_has_behavior "with a #change callback", s1, s2, result 148 | it_has_behavior "without a #change callback", s1, s2, result 149 | end 150 | 151 | describe "sequences %w(abcd efgh ijkl mnoopqrstuvwxyz) & []" do 152 | s1 = %w[abcd efgh ijkl mnopqrstuvwxyz] 153 | s2 = [] 154 | result = [ 155 | ["<", 0, 0], 156 | ["<", 1, 0], 157 | ["<", 2, 0], 158 | ["<", 3, 0] 159 | ] 160 | 161 | it_has_behavior "with a #change callback", s1, s2, result 162 | it_has_behavior "without a #change callback", s1, s2, result 163 | end 164 | 165 | describe "strings %q(a b c) & %q(a x c)" do 166 | s1 = "a b c" 167 | s2 = "a x c" 168 | 169 | result = [ 170 | ["=", 0, 0], 171 | ["=", 1, 1], 172 | ["!", 2, 2], 173 | ["=", 3, 3], 174 | ["=", 4, 4] 175 | ] 176 | 177 | it_has_behavior "with a #change callback", s1, s2, result 178 | it_has_behavior "without a #change callback", s1, s2, result 179 | end 180 | 181 | describe "strings %q(a x y c) & %q(a v w c)" do 182 | s1 = "a x y c" 183 | s2 = "a v w c" 184 | 185 | result = [ 186 | ["=", 0, 0], 187 | ["=", 1, 1], 188 | ["!", 2, 2], 189 | ["=", 3, 3], 190 | ["!", 4, 4], 191 | ["=", 5, 5], 192 | ["=", 6, 6] 193 | ] 194 | 195 | it_has_behavior "with a #change callback", s1, s2, result 196 | it_has_behavior "without a #change callback", s1, s2, result 197 | end 198 | 199 | describe "strings %q(x y c) & %q(v w c)" do 200 | s1 = "x y c" 201 | s2 = "v w c" 202 | result = [ 203 | ["!", 0, 0], 204 | ["=", 1, 1], 205 | ["!", 2, 2], 206 | ["=", 3, 3], 207 | ["=", 4, 4] 208 | ] 209 | 210 | it_has_behavior "with a #change callback", s1, s2, result 211 | it_has_behavior "without a #change callback", s1, s2, result 212 | end 213 | 214 | describe "strings %q(a x y z) & %q(b v w)" do 215 | s1 = "a x y z" 216 | s2 = "b v w" 217 | result = [ 218 | ["!", 0, 0], 219 | ["=", 1, 1], 220 | ["!", 2, 2], 221 | ["=", 3, 3], 222 | ["!", 4, 4], 223 | ["<", 5, 5], 224 | ["<", 6, 5] 225 | ] 226 | 227 | it_has_behavior "with a #change callback", s1, s2, result 228 | it_has_behavior "without a #change callback", s1, s2, result 229 | end 230 | 231 | describe "strings %q(a z) & %q(a)" do 232 | s1 = "a z" 233 | s2 = "a" 234 | result = [ 235 | ["=", 0, 0], 236 | ["<", 1, 1], 237 | ["<", 2, 1] 238 | ] 239 | 240 | it_has_behavior "with a #change callback", s1, s2, result 241 | it_has_behavior "without a #change callback", s1, s2, result 242 | end 243 | 244 | describe "strings %q(z a) & %q(a)" do 245 | s1 = "z a" 246 | s2 = "a" 247 | result = [ 248 | ["<", 0, 0], 249 | ["<", 1, 0], 250 | ["=", 2, 0] 251 | ] 252 | 253 | it_has_behavior "with a #change callback", s1, s2, result 254 | it_has_behavior "without a #change callback", s1, s2, result 255 | end 256 | 257 | describe "strings %q(a b c) & %q(x y z)" do 258 | s1 = "a b c" 259 | s2 = "x y z" 260 | result = [ 261 | ["!", 0, 0], 262 | ["=", 1, 1], 263 | ["!", 2, 2], 264 | ["=", 3, 3], 265 | ["!", 4, 4] 266 | ] 267 | 268 | it_has_behavior "with a #change callback", s1, s2, result 269 | it_has_behavior "without a #change callback", s1, s2, result 270 | end 271 | 272 | describe "strings %q(abcd efgh ijkl mnopqrstuvwxyz) & %q()" do 273 | s1 = "abcd efgh ijkl mnopqrstuvwxyz" 274 | s2 = "" 275 | # standard:disable Layout/ExtraSpacing 276 | result = [ 277 | ["<", 0, 0], 278 | ["<", 1, 0], 279 | ["<", 2, 0], 280 | ["<", 3, 0], 281 | ["<", 4, 0], 282 | ["<", 5, 0], 283 | ["<", 6, 0], 284 | ["<", 7, 0], 285 | ["<", 8, 0], 286 | ["<", 9, 0], 287 | ["<", 10, 0], 288 | ["<", 11, 0], 289 | ["<", 12, 0], 290 | ["<", 13, 0], 291 | ["<", 14, 0], 292 | ["<", 15, 0], 293 | ["<", 16, 0], 294 | ["<", 17, 0], 295 | ["<", 18, 0], 296 | ["<", 19, 0], 297 | ["<", 20, 0], 298 | ["<", 21, 0], 299 | ["<", 22, 0], 300 | ["<", 23, 0], 301 | ["<", 24, 0], 302 | ["<", 25, 0], 303 | ["<", 26, 0], 304 | ["<", 27, 0], 305 | ["<", 28, 0] 306 | ] 307 | # standard:enable Layout/ExtraSpacing 308 | 309 | it_has_behavior "with a #change callback", s1, s2, result 310 | it_has_behavior "without a #change callback", s1, s2, result 311 | end 312 | end 313 | -------------------------------------------------------------------------------- /spec/traverse_sequences_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe "Diff::LCS.traverse_sequences" do 6 | describe "callback with no finishers" do 7 | describe "over (seq1, seq2)" do 8 | before(:each) do 9 | @callback_s1_s2 = simple_callback_no_finishers 10 | Diff::LCS.traverse_sequences(seq1, seq2, @callback_s1_s2) 11 | 12 | @callback_s2_s1 = simple_callback_no_finishers 13 | Diff::LCS.traverse_sequences(seq2, seq1, @callback_s2_s1) 14 | end 15 | 16 | it "has the correct LCS result on left-matches" do 17 | expect(@callback_s1_s2.matched_a).to eq(correct_lcs) 18 | expect(@callback_s2_s1.matched_a).to eq(correct_lcs) 19 | end 20 | 21 | it "has the correct LCS result on right-matches" do 22 | expect(@callback_s1_s2.matched_b).to eq(correct_lcs) 23 | expect(@callback_s2_s1.matched_b).to eq(correct_lcs) 24 | end 25 | 26 | it "has the correct skipped sequences with the left sequence" do 27 | expect(@callback_s1_s2.discards_a).to eq(skipped_seq1) 28 | expect(@callback_s2_s1.discards_a).to eq(skipped_seq2) 29 | end 30 | 31 | it "has the correct skipped sequences with the right sequence" do 32 | expect(@callback_s1_s2.discards_b).to eq(skipped_seq2) 33 | expect(@callback_s2_s1.discards_b).to eq(skipped_seq1) 34 | end 35 | 36 | it "does not have anything done markers from the left or right sequences" do 37 | expect(@callback_s1_s2.done_a).to be_empty 38 | expect(@callback_s1_s2.done_b).to be_empty 39 | expect(@callback_s2_s1.done_a).to be_empty 40 | expect(@callback_s2_s1.done_b).to be_empty 41 | end 42 | end 43 | 44 | describe "over (hello, hello)" do 45 | before(:each) do 46 | @callback = simple_callback_no_finishers 47 | Diff::LCS.traverse_sequences(hello, hello, @callback) 48 | end 49 | 50 | it "has the correct LCS result on left-matches" do 51 | expect(@callback.matched_a).to eq(hello.chars) 52 | end 53 | 54 | it "has the correct LCS result on right-matches" do 55 | expect(@callback.matched_b).to eq(hello.chars) 56 | end 57 | 58 | it "has the correct skipped sequences with the left sequence" do 59 | expect(@callback.discards_a).to be_empty 60 | end 61 | 62 | it "has the correct skipped sequences with the right sequence" do 63 | expect(@callback.discards_b).to be_empty 64 | end 65 | 66 | it "does not have anything done markers from the left or right sequences" do 67 | expect(@callback.done_a).to be_empty 68 | expect(@callback.done_b).to be_empty 69 | end 70 | end 71 | 72 | describe "over (hello_ary, hello_ary)" do 73 | before(:each) do 74 | @callback = simple_callback_no_finishers 75 | Diff::LCS.traverse_sequences(hello_ary, hello_ary, @callback) 76 | end 77 | 78 | it "has the correct LCS result on left-matches" do 79 | expect(@callback.matched_a).to eq(hello_ary) 80 | end 81 | 82 | it "has the correct LCS result on right-matches" do 83 | expect(@callback.matched_b).to eq(hello_ary) 84 | end 85 | 86 | it "has the correct skipped sequences with the left sequence" do 87 | expect(@callback.discards_a).to be_empty 88 | end 89 | 90 | it "has the correct skipped sequences with the right sequence" do 91 | expect(@callback.discards_b).to be_empty 92 | end 93 | 94 | it "does not have anything done markers from the left or right sequences" do 95 | expect(@callback.done_a).to be_empty 96 | expect(@callback.done_b).to be_empty 97 | end 98 | end 99 | end 100 | 101 | describe "callback with finisher" do 102 | before(:each) do 103 | @callback_s1_s2 = simple_callback 104 | Diff::LCS.traverse_sequences(seq1, seq2, @callback_s1_s2) 105 | @callback_s2_s1 = simple_callback 106 | Diff::LCS.traverse_sequences(seq2, seq1, @callback_s2_s1) 107 | end 108 | 109 | it "has the correct LCS result on left-matches" do 110 | expect(@callback_s1_s2.matched_a).to eq(correct_lcs) 111 | expect(@callback_s2_s1.matched_a).to eq(correct_lcs) 112 | end 113 | 114 | it "has the correct LCS result on right-matches" do 115 | expect(@callback_s1_s2.matched_b).to eq(correct_lcs) 116 | expect(@callback_s2_s1.matched_b).to eq(correct_lcs) 117 | end 118 | 119 | it "has the correct skipped sequences for the left sequence" do 120 | expect(@callback_s1_s2.discards_a).to eq(skipped_seq1) 121 | expect(@callback_s2_s1.discards_a).to eq(skipped_seq2) 122 | end 123 | 124 | it "has the correct skipped sequences for the right sequence" do 125 | expect(@callback_s1_s2.discards_b).to eq(skipped_seq2) 126 | expect(@callback_s2_s1.discards_b).to eq(skipped_seq1) 127 | end 128 | 129 | it "has done markers differently-sized sequences" do 130 | expect(@callback_s1_s2.done_a).to eq([["p", 9, "t", 11]]) 131 | expect(@callback_s1_s2.done_b).to be_empty 132 | 133 | expect(@callback_s2_s1.done_a).to be_empty 134 | expect(@callback_s2_s1.done_b).to eq([["t", 11, "p", 9]]) 135 | end 136 | end 137 | end 138 | --------------------------------------------------------------------------------