diff --git a/lib/src/bindings/commit.dart b/lib/src/bindings/commit.dart index 5dbcae0..dc121eb 100644 --- a/lib/src/bindings/commit.dart +++ b/lib/src/bindings/commit.dart @@ -113,6 +113,56 @@ Pointer create({ } } +/// Amend an existing commit by replacing only non-null values. +/// +/// This creates a new commit that is exactly the same as the old commit, except that +/// any non-null values will be updated. The new commit has the same parents as the old commit. +/// +/// The [updateRef] value works as in the regular [create], updating the ref to point to +/// the newly rewritten commit. If you want to amend a commit that is not currently +/// the tip of the branch and then rewrite the following commits to reach a ref, pass +/// this as null and update the rest of the commit chain and ref separately. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer amend({ + required Pointer repoPointer, + required Pointer commitPointer, + String? updateRef, + required Pointer? authorPointer, + required Pointer? committerPointer, + String? messageEncoding, + required String? message, + required Pointer? treePointer, +}) { + final out = calloc(); + final updateRefC = updateRef?.toNativeUtf8().cast() ?? nullptr; + final messageEncodingC = + messageEncoding?.toNativeUtf8().cast() ?? nullptr; + final messageC = message?.toNativeUtf8().cast() ?? nullptr; + + final error = libgit2.git_commit_amend( + out, + commitPointer, + updateRefC, + authorPointer ?? nullptr, + committerPointer ?? nullptr, + messageEncodingC, + messageC, + treePointer ?? nullptr, + ); + + calloc.free(updateRefC); + calloc.free(messageEncodingC); + calloc.free(messageC); + + if (error < 0) { + calloc.free(out); + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out; + } +} + /// Get the encoding for the message of a commit, as a string representing a standard encoding name. /// /// The encoding may be NULL if the encoding header in the commit is missing; diff --git a/lib/src/commit.dart b/lib/src/commit.dart index 1f32b33..1b23329 100644 --- a/lib/src/commit.dart +++ b/lib/src/commit.dart @@ -58,6 +58,39 @@ class Commit { )); } + /// Amends an existing commit by replacing only non-null values. + /// + /// This creates a new commit that is exactly the same as the old commit, except that + /// any non-null values will be updated. The new commit has the same parents as the old commit. + /// + /// The [updateRef] value works as in the regular [create], updating the ref to point to + /// the newly rewritten commit. If you want to amend a commit that is not currently + /// the tip of the branch and then rewrite the following commits to reach a ref, pass + /// this as null and update the rest of the commit chain and ref separately. + /// + /// Throws a [LibGit2Error] if error occured. + static Oid amend({ + required Repository repo, + required Commit commit, + Signature? author, + Signature? committer, + Tree? tree, + String? updateRef, + String? message, + String? messageEncoding, + }) { + return Oid(bindings.amend( + repoPointer: repo.pointer, + commitPointer: commit.pointer, + authorPointer: author?.pointer, + committerPointer: committer?.pointer, + treePointer: tree?.pointer, + updateRef: updateRef, + message: message, + messageEncoding: messageEncoding, + )); + } + /// Returns the encoding for the message of a commit, as a string /// representing a standard encoding name. String get messageEncoding => bindings.messageEncoding(_commitPointer); diff --git a/lib/src/repository.dart b/lib/src/repository.dart index 7e210c0..d78ec09 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -509,6 +509,59 @@ class Repository { ); } + /// Amends an existing commit by replacing only non-null values. + /// + /// This creates a new commit that is exactly the same as the old commit, except that + /// any non-null values will be updated. The new commit has the same parents as the old commit. + /// + /// The [updateRef] value works as in the regular [create], updating the ref to point to + /// the newly rewritten commit. If you want to amend a commit that is not currently + /// the tip of the branch and then rewrite the following commits to reach a ref, pass + /// this as null and update the rest of the commit chain and ref separately. + /// + /// Throws a [LibGit2Error] if error occured. + Oid amendCommit({ + required Commit commit, + Signature? author, + Signature? committer, + Tree? tree, + String? updateRef, + String? message, + String? messageEncoding, + }) { + return Commit.amend( + repo: this, + commit: commit, + author: author, + committer: committer, + tree: tree, + updateRef: updateRef, + message: message, + messageEncoding: messageEncoding, + ); + } + + /// Reverts the given commit against the given "our" commit, producing an index that + /// reflects the result of the revert. + /// + /// [mainline] is parent of the [revertCommit] if it is a merge (i.e. 1, 2). + /// + /// The returned index must be freed explicitly with `free()`. + /// + /// Throws a [LibGit2Error] if error occured. + Index revertCommit({ + required Commit revertCommit, + required Commit ourCommit, + mainline = 0, + }) { + return Index(commit_bindings.revertCommit( + repoPointer: _repoPointer, + revertCommitPointer: revertCommit.pointer, + ourCommitPointer: ourCommit.pointer, + mainline: mainline, + )); + } + /// Finds a single object and intermediate reference (if there is one) by a [spec] revision string. /// /// See `man gitrevisions`, or https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions @@ -875,27 +928,6 @@ class Repository { )); } - /// Reverts the given commit against the given "our" commit, producing an index that - /// reflects the result of the revert. - /// - /// [mainline] is parent of the [revertCommit] if it is a merge (i.e. 1, 2). - /// - /// The returned index must be freed explicitly with `free()`. - /// - /// Throws a [LibGit2Error] if error occured. - Index revertCommit({ - required Commit revertCommit, - required Commit ourCommit, - mainline = 0, - }) { - return Index(commit_bindings.revertCommit( - repoPointer: _repoPointer, - revertCommitPointer: revertCommit.pointer, - ourCommitPointer: ourCommit.pointer, - mainline: mainline, - )); - } - /// Merges two trees, producing an index that reflects the result of the merge. /// The index may be written as-is to the working directory or checked out. If the index /// is to be converted to a tree, the caller should resolve any conflicts that arose as part diff --git a/test/commit_test.dart b/test/commit_test.dart index b52891d..b73fa28 100644 --- a/test/commit_test.dart +++ b/test/commit_test.dart @@ -222,6 +222,104 @@ void main() { parent.free(); }); + test('successfully amends commit with default arguments', () { + final oldHead = repo.head; + final commit = repo.lookupCommit(repo['821ed6e']); + expect(commit.oid, oldHead.target); + + final amendedOid = repo.amendCommit( + commit: commit, + message: 'amended commit\n', + updateRef: 'HEAD', + ); + final amendedCommit = repo.lookupCommit(amendedOid); + final newHead = repo.head; + + expect(amendedCommit.oid, newHead.target); + expect(amendedCommit.message, 'amended commit\n'); + expect(amendedCommit.author, commit.author); + expect(amendedCommit.committer, commit.committer); + expect(amendedCommit.tree.oid, commit.tree.oid); + expect(amendedCommit.parents, commit.parents); + + amendedCommit.free(); + commit.free(); + newHead.free(); + oldHead.free(); + }); + + test('successfully amends commit with provided arguments', () { + final oldHead = repo.head; + final commit = repo.lookupCommit(repo['821ed6e']); + expect(commit.oid, oldHead.target); + + final amendedOid = repo.amendCommit( + commit: commit, + message: 'amended commit\n', + updateRef: 'HEAD', + author: author, + committer: commiter, + tree: tree, + ); + final amendedCommit = repo.lookupCommit(amendedOid); + final newHead = repo.head; + + expect(amendedCommit.oid, newHead.target); + expect(amendedCommit.message, 'amended commit\n'); + expect(amendedCommit.author, author); + expect(amendedCommit.committer, commiter); + expect(amendedCommit.tree.oid, tree.oid); + expect(amendedCommit.parents, commit.parents); + + amendedCommit.free(); + commit.free(); + newHead.free(); + oldHead.free(); + }); + + test('successfully amends commit that is not the tip of the branch', () { + final head = repo.head; + final commit = repo.lookupCommit(repo['78b8bf1']); + expect(commit.oid, isNot(head.target)); + + expect( + () => repo.amendCommit( + commit: commit, + message: 'amended commit\n', + ), + returnsNormally, + ); + + commit.free(); + head.free(); + }); + + test( + 'throws when trying to amend commit that is not the tip of the branch ' + 'with HEAD provided as update reference', () { + final head = repo.head; + final commit = repo.lookupCommit(repo['78b8bf1']); + expect(commit.oid, isNot(head.target)); + + expect( + () => repo.amendCommit( + commit: commit, + message: 'amended commit\n', + updateRef: 'HEAD', + ), + throwsA( + isA().having( + (e) => e.toString(), + 'error', + "commit to amend is not the tip of the given branch", + ), + ), + ); + + commit.free(); + head.free(); + }); + test('returns string representation of Commit object', () { final commit = repo.lookupCommit(mergeCommit); expect(commit.toString(), contains('Commit{'));