import 'dart:ffi'; import 'package:equatable/equatable.dart'; import 'package:ffi/ffi.dart'; import 'package:libgit2dart/libgit2dart.dart'; import 'package:libgit2dart/src/bindings/diff.dart' as bindings; import 'package:libgit2dart/src/bindings/libgit2_bindings.dart'; import 'package:libgit2dart/src/util.dart'; import 'package:meta/meta.dart'; @immutable class Diff extends Equatable { /// Initializes a new instance of [Diff] class from provided /// pointer to diff object in memory. /// /// Note: For internal use. Instead, use one of: /// - [Diff.indexToWorkdir] /// - [Diff.indexToIndex] /// - [Diff.treeToIndex] /// - [Diff.treeToWorkdir] /// - [Diff.treeToWorkdirWithIndex] /// - [Diff.treeToTree] /// - [Diff.parse] Diff(this._diffPointer) { _finalizer.attach(this, _diffPointer, detach: this); } /// Creates a diff between the [repo]sitory [index] and the workdir directory. /// /// This matches the `git diff` command. /// /// [repo] is the repository containing index. /// /// [index] is the index to diff from. /// /// [flags] is a combination of [GitDiff] flags. Defaults to [GitDiff.normal]. /// /// [contextLines] is the number of unchanged lines that define the boundary /// of a hunk (and to display before and after). Defaults to 3. /// /// [interhunkLines] is the maximum number of unchanged lines between hunk /// boundaries before the hunks will be merged into one. Defaults to 0. /// /// Throws a [LibGit2Error] if error occured. Diff.indexToWorkdir({ required Repository repo, required Index index, Set flags = const {GitDiff.normal}, int contextLines = 3, int interhunkLines = 0, }) { _diffPointer = bindings.indexToWorkdir( repoPointer: repo.pointer, indexPointer: index.pointer, flags: flags.fold(0, (int acc, e) => acc | e.value), contextLines: contextLines, interhunkLines: interhunkLines, ); _finalizer.attach(this, _diffPointer, detach: this); } /// Creates a diff between a [tree] and [repo]sitory [index]. /// /// This is equivalent to `git diff --cached ` or if you pass the /// HEAD tree, then like `git diff --cached`. /// /// [repo] is the repository containing the tree and index. /// /// [tree] is the [Tree] object to diff from or null for empty tree. /// /// [index] is the index to diff with. /// /// [flags] is a combination of [GitDiff] flags. Defaults to [GitDiff.normal]. /// /// [contextLines] is the number of unchanged lines that define the boundary /// of a hunk (and to display before and after). Defaults to 3. /// /// [interhunkLines] is the maximum number of unchanged lines between hunk /// boundaries before the hunks will be merged into one. Defaults to 0. Diff.treeToIndex({ required Repository repo, required Tree? tree, required Index index, Set flags = const {GitDiff.normal}, int contextLines = 3, int interhunkLines = 0, }) { _diffPointer = bindings.treeToIndex( repoPointer: repo.pointer, treePointer: tree?.pointer, indexPointer: index.pointer, flags: flags.fold(0, (acc, e) => acc | e.value), contextLines: contextLines, interhunkLines: interhunkLines, ); _finalizer.attach(this, _diffPointer, detach: this); } /// Creates a diff between a [tree] and the working directory. /// /// This is not the same as `git diff ` or /// `git diff-index `. Those commands use information from the /// index, whereas this method strictly returns the differences between the /// tree and the files in the working directory, regardless of the state of /// the index. Use [Diff.treeToWorkdirWithIndex] to emulate those commands. /// /// To see difference between this and [Diff.treeToWorkdirWithIndex], /// consider the example of a staged file deletion where the file has then /// been put back into the working directory and further modified. The /// tree-to-workdir diff for that file is 'modified', but `git diff` would /// show status 'deleted' since there is a staged delete. /// /// [repo] is the repository containing the tree. /// /// [tree] is the [Tree] object to diff from or null for empty tree. /// /// [flags] is a combination of [GitDiff] flags. Defaults to [GitDiff.normal]. /// /// [contextLines] is the number of unchanged lines that define the boundary /// of a hunk (and to display before and after). Defaults to 3. /// /// [interhunkLines] is the maximum number of unchanged lines between hunk /// boundaries before the hunks will be merged into one. Defaults to 0. Diff.treeToWorkdir({ required Repository repo, required Tree? tree, Set flags = const {GitDiff.normal}, int contextLines = 3, int interhunkLines = 0, }) { _diffPointer = bindings.treeToWorkdir( repoPointer: repo.pointer, treePointer: tree?.pointer, flags: flags.fold(0, (acc, e) => acc | e.value), contextLines: contextLines, interhunkLines: interhunkLines, ); _finalizer.attach(this, _diffPointer, detach: this); } /// Creates a diff between a [tree] and the working directory using index /// data to account for staged deletes, tracked files, etc. /// /// This emulates `git diff ` by diffing the tree to the index and the /// index to the working directory and blending the results into a single diff /// that includes staged deleted, etc. /// /// [repo] is the repository containing the tree. /// /// [tree] is a [Tree] object to diff from, or null for empty tree. /// /// [flags] is a combination of [GitDiff] flags. Defaults to [GitDiff.normal]. /// /// [contextLines] is the number of unchanged lines that define the boundary /// of a hunk (and to display before and after). Defaults to 3. /// /// [interhunkLines] is the maximum number of unchanged lines between hunk /// boundaries before the hunks will be merged into one. Defaults to 0. /// /// Throws a [LibGit2Error] if error occured. Diff.treeToWorkdirWithIndex({ required Repository repo, required Tree? tree, Set flags = const {GitDiff.normal}, int contextLines = 3, int interhunkLines = 0, }) { _diffPointer = bindings.treeToWorkdirWithIndex( repoPointer: repo.pointer, treePointer: tree?.pointer, flags: flags.fold(0, (acc, e) => acc | e.value), contextLines: contextLines, interhunkLines: interhunkLines, ); _finalizer.attach(this, _diffPointer, detach: this); } /// Creates a diff with the difference between two [Tree] objects. /// /// This is equivalent to `git diff `. /// /// [repo] is the repository containing the trees. /// /// [oldTree] is the [Tree] object to diff from, or null for empty tree. /// /// [newTree] is the [Tree] object to diff to, or null for empty tree. /// /// [flags] is a combination of [GitDiff] flags. Defaults to [GitDiff.normal]. /// /// [contextLines] is the number of unchanged lines that define the boundary /// of a hunk (and to display before and after). Defaults to 3. /// /// [interhunkLines] is the maximum number of unchanged lines between hunk /// boundaries before the hunks will be merged into one. Defaults to 0. /// /// Throws a [LibGit2Error] if error occured or [ArgumentError] if both trees /// are null. Diff.treeToTree({ required Repository repo, required Tree? oldTree, required Tree? newTree, Set flags = const {GitDiff.normal}, int contextLines = 3, int interhunkLines = 0, }) { if (oldTree == null && newTree == null) { throw ArgumentError('Both trees cannot be null'); } _diffPointer = bindings.treeToTree( repoPointer: repo.pointer, oldTreePointer: oldTree?.pointer, newTreePointer: newTree?.pointer, flags: flags.fold(0, (acc, e) => acc | e.value), contextLines: contextLines, interhunkLines: interhunkLines, ); _finalizer.attach(this, _diffPointer, detach: this); } /// Creates a diff with the difference between two [Index] objects. /// /// [repo] is the repository containing the indexes. /// /// [oldIndex] is the [Index] object to diff from. /// /// [newIndex] is the [Index] object to diff to. /// /// [flags] is a combination of [GitDiff] flags. Defaults to [GitDiff.normal]. /// /// [contextLines] is the number of unchanged lines that define the boundary /// of a hunk (and to display before and after). Defaults to 3. /// /// [interhunkLines] is the maximum number of unchanged lines between hunk /// boundaries before the hunks will be merged into one. Defaults to 0. /// /// Throws a [LibGit2Error] if error occured. Diff.indexToIndex({ required Repository repo, required Index oldIndex, required Index newIndex, Set flags = const {GitDiff.normal}, int contextLines = 3, int interhunkLines = 0, }) { _diffPointer = bindings.indexToIndex( repoPointer: repo.pointer, oldIndexPointer: oldIndex.pointer, newIndexPointer: newIndex.pointer, flags: flags.fold(0, (acc, e) => acc | e.value), contextLines: contextLines, interhunkLines: interhunkLines, ); _finalizer.attach(this, _diffPointer, detach: this); } /// Reads the [content]s of a git patch file into a git diff object. /// /// The diff object produced is similar to the one that would be produced if /// you actually produced it computationally by comparing two trees, however /// there may be subtle differences. For example, a patch file likely /// contains abbreviated object IDs, so the object IDs in a diff delta /// produced by this function will also be abbreviated. /// /// This function will only read patch files created by a git implementation, /// it will not read unified diffs produced by the `diff` program, nor any /// other types of patch files. Diff.parse(String content) { libgit2.git_libgit2_init(); _diffPointer = bindings.parse(content); _finalizer.attach(this, _diffPointer, detach: this); } late final Pointer _diffPointer; /// Pointer to memory address for allocated diff object. /// /// Note: For internal use. Pointer get pointer => _diffPointer; /// How many diff records are there in a diff. int get length => bindings.length(_diffPointer); /// Returns a list of [DiffDelta]s containing file pairs with and old and new /// revisions. List get deltas { final length = bindings.length(_diffPointer); return [ for (var i = 0; i < length; i++) DiffDelta(bindings.getDeltaByIndex(diffPointer: _diffPointer, index: i)) ]; } /// A List of [Patch]es. List get patches { final length = bindings.length(_diffPointer); return [ for (var i = 0; i < length; i++) Patch.fromDiff(diff: this, index: i) ]; } /// The patch diff text. String get patch => bindings.addToBuf(_diffPointer); /// Accumulates diff statistics for all patches. /// /// Throws a [LibGit2Error] if error occured. DiffStats get stats => DiffStats(bindings.stats(_diffPointer)); /// Merges one diff into another. void merge(Diff diff) { bindings.merge( ontoPointer: _diffPointer, fromPointer: diff.pointer, ); } /// Applies the diff to the [repo]sitory, making changes in the provided /// [location]. /// /// [repo] is the repository to apply to. /// /// [hunkIndex] is optional index of the hunk to apply. /// /// [location] is the location to apply (workdir, index or both). /// Defaults to workdir. /// /// Throws a [LibGit2Error] if error occured. void apply({ required Repository repo, int? hunkIndex, GitApplyLocation location = GitApplyLocation.workdir, }) { bindings.apply( repoPointer: repo.pointer, diffPointer: _diffPointer, hunkIndex: hunkIndex, location: location.value, ); } /// Checks if the diff will apply to provided [location]. /// /// [repo] is the repository to apply to. /// /// [hunkIndex] is optional index of the hunk to apply. /// /// [location] is the location to apply (workdir, index or both). /// Defaults to workdir. bool applies({ required Repository repo, int? hunkIndex, GitApplyLocation location = GitApplyLocation.workdir, }) { return bindings.apply( repoPointer: repo.pointer, diffPointer: _diffPointer, hunkIndex: hunkIndex, location: location.value, check: true, ); } /// Applies the diff to the [tree], and returns the resulting image as an /// index. /// /// [repo] is the repository to apply to. /// /// [tree] is the tree to apply the diff to. /// /// [hunkIndex] is optional index of the hunk to apply. /// /// Throws a [LibGit2Error] if error occured. Index applyToTree({ required Repository repo, required Tree tree, int? hunkIndex, }) { return Index( bindings.applyToTree( repoPointer: repo.pointer, diffPointer: _diffPointer, treePointer: tree.pointer, hunkIndex: hunkIndex, ), ); } /// Transforms a diff marking file renames, copies, etc. /// /// This modifies a diff in place, replacing old entries that look like /// renames or copies with new entries reflecting those changes. This also /// will, if requested, break modified files into add/remove pairs if the /// amount of change is above a threshold. /// /// [flags] is a combination of [GitDiffFind] flags. Defaults to /// [GitDiffFind.byConfig]. /// /// [renameThreshold] is the threshold above which similar files will be /// considered renames. This is equivalent to the -M option. Defaults to 50. /// /// [copyThreshold] is the threshold above which similar files will be /// considered copies. This is equivalent to the -C option. Defaults to 50. /// /// [renameFromRewriteThreshold] is the threshold below which similar files /// will be eligible to be a rename source. This is equivalent to the first /// part of the -B option. Defaults to 50. /// /// [breakRewriteThreshold] is the treshold below which similar files will be /// split into a delete/add pair. This is equivalent to the last part of the -B /// option. Defaults to 60. /// /// [renameLimit] is the maximum number of matches to consider for a /// particular file. This is a little different from the -l option from Git /// because we will still process up to this many matches before abandoning /// the search. Defaults to 200. /// /// Throws a [LibGit2Error] if error occured. void findSimilar({ Set flags = const {GitDiffFind.byConfig}, int renameThreshold = 50, int copyThreshold = 50, int renameFromRewriteThreshold = 50, int breakRewriteThreshold = 60, int renameLimit = 200, }) { bindings.findSimilar( diffPointer: _diffPointer, flags: flags.fold(0, (acc, e) => acc | e.value), renameThreshold: renameThreshold, copyThreshold: copyThreshold, renameFromRewriteThreshold: renameFromRewriteThreshold, breakRewriteThreshold: breakRewriteThreshold, renameLimit: renameLimit, ); } /// Calculates a stable patch [Oid] for the given patch by summing the hash /// of the file diffs, ignoring whitespace and line numbers. This can be used /// to derive whether two diffs are the same with a high probability. /// /// Currently, this function only calculates stable patch IDs, as defined in /// `git-patch-id(1)`, and should in fact generate the same IDs as the /// upstream git project does. /// /// Throws a [LibGit2Error] if error occured. Oid get patchOid => Oid(bindings.patchOid(_diffPointer)); /// Releases memory allocated for diff object. void free() { bindings.free(_diffPointer); _finalizer.detach(this); } @override String toString() { return 'Diff{length: $length, patchOid: $patchOid}'; } @override List get props => [patchOid]; } // coverage:ignore-start final _finalizer = Finalizer>( (pointer) => bindings.free(pointer), ); // coverage:ignore-end @immutable class DiffDelta extends Equatable { /// Initializes a new instance of [DiffDelta] class from provided /// pointer to diff delta object in memory. const DiffDelta(this._diffDeltaPointer); /// Pointer to memory address for allocated diff delta object. final Pointer _diffDeltaPointer; /// Type of change. GitDelta get status { return GitDelta.values.firstWhere( (e) => _diffDeltaPointer.ref.status == e.value, ); } /// Single character abbreviation for a delta status code. /// /// When you run `git diff --name-status` it uses single letter codes in the /// output such as 'A' for added, 'D' for deleted, 'M' for modified, etc. /// This function converts a [GitDelta] value into these letters for your own /// purposes. [GitDelta.untracked] will return a space (i.e. ' '). String get statusChar => bindings.statusChar(_diffDeltaPointer.ref.status); /// Flags for the delta object. Set get flags { return GitDiffFlag.values .where((e) => _diffDeltaPointer.ref.flags & e.value == e.value) .toSet(); } /// Similarity score for renamed or copied files between 0 and 100 /// indicating how similar the old and new sides are. int get similarity => _diffDeltaPointer.ref.similarity; /// Number of files in this delta. int get numberOfFiles => _diffDeltaPointer.ref.nfiles; /// Represents the "from" side of the diff. DiffFile get oldFile => DiffFile._(_diffDeltaPointer.ref.old_file); /// Represents the "to" side of the diff. DiffFile get newFile => DiffFile._(_diffDeltaPointer.ref.new_file); @override String toString() { return 'DiffDelta{status: $status, flags: $flags, similarity: $similarity, ' 'numberOfFiles: $numberOfFiles, oldFile: $oldFile, newFile: $newFile}'; } @override List get props => [ status, flags, similarity, numberOfFiles, oldFile, newFile, ]; } /// Description of one side of a delta. /// /// Although this is called a "file", it could represent a file, a symbolic /// link, a submodule commit id, or even a tree (although that only if you /// are tracking type changes or ignored/untracked directories). @immutable class DiffFile extends Equatable { /// Initializes a new instance of [DiffFile] class from provided diff file /// object. const DiffFile._(this._diffFile); final git_diff_file _diffFile; /// [Oid] of the item. If the entry represents an absent side of a diff /// then the oid will be zeroes. Oid get oid => Oid.fromRaw(_diffFile.id); /// Path to the entry relative to the working directory of the repository. String get path => _diffFile.path.cast().toDartString(); /// Size of the entry in bytes. int get size => _diffFile.size; /// Flags for the diff file object. Set get flags { return GitDiffFlag.values .where((e) => _diffFile.flags & e.value == e.value) .toSet(); } /// One of the [GitFilemode] values. GitFilemode get mode { return GitFilemode.values.firstWhere((e) => _diffFile.mode == e.value); } @override String toString() { return 'DiffFile{oid: $oid, path: $path, size: $size, flags: $flags, ' 'mode: $mode}'; } @override List get props => [oid, path, size, flags, mode]; } class DiffStats { /// Initializes a new instance of [DiffStats] class from provided /// pointer to diff stats object in memory. /// /// Note: For internal use. DiffStats(this._diffStatsPointer) { _statsFinalizer.attach(this, _diffStatsPointer, detach: this); } /// Pointer to memory address for allocated diff delta object. final Pointer _diffStatsPointer; /// Total number of insertions. int get insertions => bindings.statsInsertions(_diffStatsPointer); /// Total number of deletions. int get deletions => bindings.statsDeletions(_diffStatsPointer); /// Total number of files changed. int get filesChanged => bindings.statsFilesChanged(_diffStatsPointer); /// Prints diff statistics. /// /// Width for output only affects formatting of [GitDiffStats.full]. /// /// Throws a [LibGit2Error] if error occured. String print({required Set format, required int width}) { return bindings.statsPrint( statsPointer: _diffStatsPointer, format: format.fold(0, (acc, e) => acc | e.value), width: width, ); } /// Releases memory allocated for diff stats object. void free() { bindings.freeStats(_diffStatsPointer); _statsFinalizer.detach(this); } @override String toString() { return 'DiffStats{insertions: $insertions, deletions: $deletions, ' 'filesChanged: $filesChanged}'; } } // coverage:ignore-start final _statsFinalizer = Finalizer>( (pointer) => bindings.freeStats(pointer), ); // coverage:ignore-end