Module: Git::Repository::Diffing

Included in:
Git::Repository
Defined in:
lib/git/repository/diffing.rb

Overview

Facade methods for comparing commits and trees using git diff

Included by Git::Repository.

Instance Method Summary collapse

Instance Method Details

#diff(obj1 = 'HEAD', obj2 = nil) ⇒ Git::Diff

Returns a lazy Diff object for the comparison between two trees

Compares (1) two commits, (2) a commit against the working tree, or (3) the index against the working tree. The returned Diff is lazy — it does not run any git commands until an accessor method (e.g., Diff#patch, Diff#each) is called.

Use Diff#path to limit the diff to a sub-path after construction.

Examples:

Get the diff since HEAD

diff = repo.diff
diff.patch  #=> "diff --git a/lib/foo.rb ..."

Compare two specific commits

repo.diff('abc1234', 'def5678').patch

Limit to a sub-path

repo.diff('HEAD~1', 'HEAD').path('lib/').patch

Get unstaged changes (index vs. working tree)

repo.diff(nil).patch

Parameters:

  • obj1 (String, nil) (defaults to: 'HEAD')

    the first commit or object to compare; defaults to 'HEAD'

  • obj2 (String, nil) (defaults to: nil)

    the second commit or object to compare

Returns:

  • (Git::Diff)

    a lazy diff object for the comparison

See Also:



291
292
293
# File 'lib/git/repository/diffing.rb', line 291

def diff(obj1 = 'HEAD', obj2 = nil)
  Git::Diff.new(self, obj1, obj2)
end

#diff_filesHash{String => Hash}

Note:

The field names in the returned hash are legacy names inherited from Git::Lib#diff_files and appear counterintuitive: :mode_repo and :sha_repo hold index (staging area) values, while :mode_index and :sha_index hold working tree values.

Compares the index and the working directory

Runs git diff-files to list files that differ between the index (staging area) and the working directory. These are changes that have been made to tracked files but not yet staged.

Examples:

List all files with unstaged changes

repo.diff_files
#=> {
#     "lib/foo.rb" => {
#       mode_index: "100644", mode_repo: "100644",
#       path: "lib/foo.rb", sha_repo: "abc1234",
#       sha_index: "0000000000000000000000000000000000000000",
#       type: "M"
#     }
#   }

Returns:

  • (Hash{String => Hash})

    a hash keyed by file path

    Each value is a hash with the following keys (note the legacy naming where :*_repo holds index data and :*_index holds working tree data):

    • :mode_index [String] the working tree file mode (legacy name)
    • :mode_repo [String] the index (staging area) file mode (legacy name)
    • :path [String] the file path
    • :sha_repo [String] the SHA of the object in the index (staging area) (legacy name)
    • :sha_index [String] the SHA of the object in the working tree; all zeros when git has not computed the working tree blob SHA (legacy name)
    • :type [String] the status code (e.g. "M", "A", "D")

Raises:

See Also:



423
424
425
426
427
428
# File 'lib/git/repository/diffing.rb', line 423

def diff_files
  Git::Commands::Status.new(@execution_context).call
  Private.parse_diff_files_output(
    Git::Commands::DiffFiles.new(@execution_context).call.stdout
  )
end

#diff_full(obj1 = 'HEAD', obj2 = nil, opts = {}) ⇒ String

Returns the full unified diff patch text between two trees

Compares (1) two commits, (2) a commit against the working tree, or (3) the index against the working tree using git diff -p, and returns the raw unified diff patch output.

Comparing two commits

When both obj1 and obj2 are provided, the comparison is between those two refs (commits, tags, branches, etc.).

Comparing a commit against the working tree

When only obj1 is provided (and isn't nil), the comparison is between obj1 and the working tree; the patch reflects all changes since obj1.

Comparing the index against the working tree

When obj1 is explicitly nil then obj2 must be omitted or nil. In this case, the comparison is between the index and the working tree; the patch reflects unstaged changes.

Examples:

Get the working tree patch since HEAD

repo.diff_full #=> "diff --git a/lib/foo.rb b/lib/foo.rb\n..."

Compare two specific commits

repo.diff_full('abc1234', 'def5678')

Get unstaged changes (index vs. working tree)

repo.diff_full(nil)

Limit the diff to a sub-path

repo.diff_full('HEAD~1', 'HEAD', path_limiter: 'lib/')

Parameters:

  • obj1 (String, nil) (defaults to: 'HEAD')

    the first commit or object to compare; defaults to 'HEAD'

  • obj2 (String, nil) (defaults to: nil)

    the second commit or object to compare

  • opts (Hash) (defaults to: {})

    options to filter the diff

Options Hash (opts):

  • :path_limiter (String, Pathname, Array<String, Pathname>, nil) — default: nil

    limit the diff to the given path(s)

Returns:

  • (String)

    the unified diff patch output

Raises:

  • (ArgumentError)

    if unsupported options are provided

  • (ArgumentError)

    if obj1 is nil but obj2 is not OR if obj1 or obj2 starts with "-"

  • (Git::FailedError)

    if git exits outside the allowed range (exit code > 2)

See Also:



86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/git/repository/diffing.rb', line 86

def diff_full(obj1 = 'HEAD', obj2 = nil, opts = {})
  SharedPrivate.assert_valid_opts!(DIFF_FULL_ALLOWED_OPTS, **opts)
  raise ArgumentError, 'Invalid arguments: obj1 is nil but obj2 is not' if obj1.nil? && !obj2.nil?

  pathspecs = Private.normalize_pathspecs(opts[:path_limiter], 'path limiter')
  result = Git::Commands::Diff.new(@execution_context).call(
    *[obj1, obj2].compact,
    patch: true, numstat: true, shortstat: true,
    src_prefix: 'a/', dst_prefix: 'b/',
    path: pathspecs
  )
  Private.extract_patch_text(result.stdout)
end

#diff_index(treeish) ⇒ Hash{String => Hash}

Note:

git diff-index without --cached uses the index as a stat cache: any file whose index entry differs from the tree is reported as changed, even when the on-disk working-tree content is byte-for-byte identical to the tree. A staged change that has been reverted in the working tree will therefore still appear in the result (because the index still differs from the tree).

Note:

The field names in the returned hash are legacy names inherited from Git::Lib#diff_index and appear counterintuitive: :mode_repo and :sha_repo hold tree (treeish) values, while :mode_index and :sha_index hold working tree values.

Compares the working tree against the given tree object

Runs git diff-index <treeish> (without --cached) to list files that differ between the given tree object (e.g. a commit or "HEAD") and the working tree. The index is refreshed via git status first so that cached stat information is up to date.

This is equivalent to the 4.x Git::Lib#diff_index behavior, which also ran git diff-index without --cached.

Examples:

List all working-tree files that differ from HEAD

repo.diff_index('HEAD')
#=> {
#     "lib/foo.rb" => {
#       mode_index: "100644", mode_repo: "100644",
#       path: "lib/foo.rb", sha_repo: "abc1234",
#       sha_index: "0000000000000000000000000000000000000000",
#       type: "M"
#     }
#   }

Parameters:

  • treeish (String)

    the tree object to compare against (e.g. 'HEAD', a commit SHA, or a tag name)

Returns:

  • (Hash{String => Hash})

    a hash keyed by file path

    Each value is a hash with the following keys (note the legacy naming where :*_repo holds tree data and :*_index holds working tree data):

    • :mode_index [String] the working tree file mode (legacy name)
    • :mode_repo [String] the tree (treeish) file mode (legacy name)
    • :path [String] the file path
    • :sha_repo [String] the SHA of the object in the tree (treeish) (legacy name)
    • :sha_index [String] the SHA of the object in the working tree; all zeros when git has not yet computed the working tree blob SHA (legacy name)
    • :type [String] the status code (e.g. "M", "A", "D")

Raises:

See Also:



483
484
485
486
487
488
# File 'lib/git/repository/diffing.rb', line 483

def diff_index(treeish)
  Git::Commands::Status.new(@execution_context).call
  Private.parse_diff_files_output(
    Git::Commands::DiffIndex.new(@execution_context).call(treeish).stdout
  )
end

#diff_numstat(obj1 = 'HEAD', obj2 = nil, opts = {}) ⇒ Hash

Returns per-file insertion/deletion counts and totals between two trees

Compares (1) two commits, (2) a commit against the working tree, or (3) the index against the working tree using git diff --numstat, and returns a structured hash of per-file insertion and deletion line counts together with aggregate totals.

Comparing two commits

When both obj1 and obj2 are provided, the comparison is between those two refs (commits, tags, branches, etc.).

Comparing a commit against the working tree

When only obj1 is provided (and isn't nil), the comparison is between obj1 and the working tree; the stats reflect all changes since obj1.

Comparing the index against the working tree

When obj1 is explicitly nil then obj2 must be omitted or nil. In this case, the comparison is between the index and the working tree; the stats reflect unstaged changes.

Examples:

Compare two specific commits

repo.diff_numstat('abc1234', 'def5678')

Get working tree changes since HEAD

repo.diff_numstat #=> {
  total: { insertions: 5, deletions: 2, lines: 7, files: 1 },
  files: { "lib/foo.rb" => { insertions: 5, deletions: 2 } }
}

Get unstaged changes (index vs. working tree)

repo.diff_numstat(nil) #=> { ... }

Limit the stats to a sub-path

repo.diff_numstat('HEAD~1', 'HEAD', path_limiter: 'lib/')

Parameters:

  • obj1 (String, nil) (defaults to: 'HEAD')

    the first commit or object to compare; defaults to 'HEAD'

  • obj2 (String, nil) (defaults to: nil)

    the second commit or object to compare

  • opts (Hash) (defaults to: {})

    options to filter the diff

Options Hash (opts):

  • :path_limiter (String, Pathname, Array<String, Pathname>, nil) — default: nil

    limit the stats to the given path(s)

Returns:

  • (Hash)

    per-file insertion and deletion counts plus aggregate totals

    {
      total: { insertions: Integer, deletions: Integer, lines: Integer, files: Integer },
      files: { "path/to/file" => { insertions: Integer, deletions: Integer } }
    }
    

Raises:

  • (ArgumentError)

    if unsupported options are provided

  • (ArgumentError)

    if obj1 is nil but obj2 is not OR if obj1 or obj2 starts with "-"

  • (Git::FailedError)

    if git exits outside the allowed range (exit code > 2)

See Also:



174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/git/repository/diffing.rb', line 174

def diff_numstat(obj1 = 'HEAD', obj2 = nil, opts = {})
  SharedPrivate.assert_valid_opts!(DIFF_NUMSTAT_ALLOWED_OPTS, **opts)
  raise ArgumentError, 'Invalid arguments: obj1 is nil but obj2 is not' if obj1.nil? && !obj2.nil?

  pathspecs = Private.normalize_pathspecs(opts[:path_limiter], 'path limiter')
  result = Git::Commands::Diff.new(@execution_context).call(
    *[obj1, obj2].compact,
    numstat: true, shortstat: true, src_prefix: 'a/', dst_prefix: 'b/',
    path: pathspecs
  )
  Private.parse_numstat_output(result.stdout)
end

#diff_path_status(from = 'HEAD', to = nil, opts = {}) ⇒ Git::DiffPathStatus Also known as: diff_name_status

Returns the file path status between two trees

Compares (1) two commits, (2) a commit against the working tree, or (3) the index against the working tree and returns a DiffPathStatus enumerating each changed file together with its status code (e.g. "M" for modified, "A" for added, "D" for deleted, "R100" for a rename with 100% similarity, etc.).

Comparing two commits

When both from and to are provided, the comparison is between those two refs (commits, tags, branches, etc.).

Comparing a commit against the working tree

When only from is provided (and isn't nil), the comparison is between from and the working tree; the status reflects all changes since from.

Comparing the index against the working tree

When from is explicitly nil then to must be omitted or nil. In this case, the comparison is between the index and the working tree; the status reflects unstaged changes.

Examples:

Get working tree path changes since HEAD

repo.diff_path_status #=> #<Git::DiffPathStatus ...>
repo.diff_path_status.to_h #=> { "README.md" => "M", "lib/foo.rb" => "A" }

Compare two specific commits

repo.diff_path_status('abc1234', 'def5678').to_h

Get unstaged path changes (index vs. working tree)

repo.diff_path_status(nil).to_h

Limit the comparison to a sub-path

repo.diff_path_status('HEAD~1', 'HEAD', path_limiter: 'lib/')

Parameters:

  • from (String, nil) (defaults to: 'HEAD')

    the first commit or object to compare; defaults to 'HEAD'

  • to (String, nil) (defaults to: nil)

    the second commit or object to compare

  • opts (Hash) (defaults to: {})

    options to filter the diff

Options Hash (opts):

  • :path_limiter (String, Pathname, Array<String, Pathname>, nil) — default: nil

    limit the status report to the given path(s)

  • :path (String, Pathname, Array<String, Pathname>, nil) — default: nil

    deprecated — use :path_limiter instead

Returns:

Raises:

  • (ArgumentError)

    if unsupported options are provided

  • (ArgumentError)

    if from is nil but to is not OR if from or to starts with "-"

  • (Git::FailedError)

    if git exits outside the allowed range (exit code > 2)

See Also:



364
365
366
367
368
369
370
371
372
373
# File 'lib/git/repository/diffing.rb', line 364

def diff_path_status(from = 'HEAD', to = nil, opts = {})
  SharedPrivate.assert_valid_opts!(DIFF_PATH_STATUS_ALLOWED_OPTS, **opts)
  raise ArgumentError, 'Invalid arguments: `from` is nil but `to` is not' if from.nil? && !to.nil?

  path_limiter = Private.resolve_path_limiter(opts)
  pathspecs = Private.normalize_pathspecs(path_limiter, 'path limiter')

  result = Private.call_diff_command(@execution_context, from, to, pathspecs)
  Git::DiffPathStatus.new(Private.extract_name_status_from_raw(result.stdout))
end

#diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {}) ⇒ Git::DiffStats

Returns the stats between two trees as a DiffStats object

Compares (1) two commits, (2) a commit against the working tree, or (3) the index against the working tree and constructs a lazy DiffStats that computes per-file insertion and deletion counts on demand when its accessor methods are called.

Comparing two commits

When both obj1 and obj2 are provided, the comparison is between those two refs (commits, tags, branches, etc.).

Comparing a commit against the working tree

When only obj1 is provided (and isn't nil), the comparison is between obj1 and the working tree; the stats reflect all changes since obj1.

Comparing the index against the working tree

When obj1 is explicitly nil then obj2 must be omitted or nil. In this case, the comparison is between the index and the working tree; the stats reflect unstaged changes.

Examples:

Get working tree stats since HEAD

stats = repo.diff_stats
stats.insertions #=> 3
stats.deletions  #=> 1

Compare two specific commits

repo.diff_stats('abc1234', 'def5678')

Get unstaged stats (index vs. working tree)

repo.diff_stats(nil).insertions

Limit stats to a sub-path

repo.diff_stats('HEAD~1', 'HEAD', path_limiter: 'lib/')

Parameters:

  • obj1 (String, nil) (defaults to: 'HEAD')

    the first commit or object to compare; defaults to 'HEAD'

  • obj2 (String, nil) (defaults to: nil)

    the second commit or object to compare

  • opts (Hash) (defaults to: {})

    options to filter the diff

Options Hash (opts):

  • :path_limiter (String, Pathname, Array<String, Pathname>, nil) — default: nil

    limit the stats to the given path(s)

Returns:

Raises:

  • (ArgumentError)

    if unsupported options are provided

  • (ArgumentError)

    if obj1 is nil but obj2 is not OR if obj1 or obj2 starts with "-"

See Also:



253
254
255
256
257
258
# File 'lib/git/repository/diffing.rb', line 253

def diff_stats(obj1 = 'HEAD', obj2 = nil, opts = {})
  SharedPrivate.assert_valid_opts!(DIFF_STATS_ALLOWED_OPTS, **opts)
  raise ArgumentError, 'Invalid arguments: obj1 is nil but obj2 is not' if obj1.nil? && !obj2.nil?

  Git::DiffStats.new(self, obj1, obj2, opts[:path_limiter])
end