diff --git a/lib/libgit2dart.dart b/lib/libgit2dart.dart index 74445e6..ffc5d20 100644 --- a/lib/libgit2dart.dart +++ b/lib/libgit2dart.dart @@ -13,5 +13,6 @@ export 'src/blob.dart'; export 'src/tag.dart'; export 'src/treebuilder.dart'; export 'src/branch.dart'; +export 'src/worktree.dart'; export 'src/error.dart'; export 'src/git_types.dart'; diff --git a/lib/src/bindings/worktree.dart b/lib/src/bindings/worktree.dart new file mode 100644 index 0000000..a610848 --- /dev/null +++ b/lib/src/bindings/worktree.dart @@ -0,0 +1,94 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'package:libgit2dart/libgit2dart.dart'; +import 'libgit2_bindings.dart'; +import '../error.dart'; +import '../util.dart'; + +/// Add a new working tree. +/// +/// Add a new working tree for the repository, that is create the required +/// data structures inside the repository and check out the current HEAD at path. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer create( + Pointer repo, + String name, + String path, +) { + final out = calloc>(); + final nameC = name.toNativeUtf8().cast(); + final pathC = path.toNativeUtf8().cast(); + final error = libgit2.git_worktree_add(out, repo, nameC, pathC, nullptr); + + calloc.free(nameC); + calloc.free(pathC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Lookup a working tree by its name for a given repository. +/// +/// Throws a [LibGit2Error] if error occured. +Pointer lookup(Pointer repo, String name) { + final out = calloc>(); + final nameC = name.toNativeUtf8().cast(); + final error = libgit2.git_worktree_lookup(out, repo, nameC); + + calloc.free(nameC); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + return out.value; + } +} + +/// Prune working tree. +/// +/// Prune the working tree, that is remove the git data structures on disk. +/// +/// Throws a [LibGit2Error] if error occured. +void prune(Pointer wt) { + final error = libgit2.git_worktree_prune(wt, nullptr); + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } +} + +/// List names of linked working trees. +/// +/// Throws a [LibGit2Error] if error occured. +List list(Pointer repo) { + final out = calloc(); + final error = libgit2.git_worktree_list(out, repo); + final result = []; + + if (error < 0) { + throw LibGit2Error(libgit2.git_error_last()); + } else { + for (var i = 0; i < out.ref.count; i++) { + result.add(out.ref.strings[i].cast().toDartString()); + } + calloc.free(out); + return result; + } +} + +/// Retrieve the name of the worktree. +String name(Pointer wt) { + return libgit2.git_worktree_name(wt).cast().toDartString(); +} + +/// Retrieve the filesystem path for the worktree. +String path(Pointer wt) { + return libgit2.git_worktree_path(wt).cast().toDartString(); +} + +/// Free a previously allocated worktree. +void free(Pointer wt) => libgit2.git_worktree_free(wt); diff --git a/lib/src/repository.dart b/lib/src/repository.dart index 5b632f2..ea889aa 100644 --- a/lib/src/repository.dart +++ b/lib/src/repository.dart @@ -1,10 +1,9 @@ import 'dart:ffi'; -import 'package:libgit2dart/libgit2dart.dart'; - import 'bindings/libgit2_bindings.dart'; import 'bindings/repository.dart' as bindings; import 'bindings/merge.dart' as merge_bindings; import 'bindings/object.dart' as object_bindings; +import 'branch.dart'; import 'commit.dart'; import 'config.dart'; import 'index.dart'; @@ -17,6 +16,7 @@ import 'blob.dart'; import 'git_types.dart'; import 'signature.dart'; import 'tag.dart'; +import 'tree.dart'; import 'util.dart'; class Repository { diff --git a/lib/src/worktree.dart b/lib/src/worktree.dart new file mode 100644 index 0000000..e15a0b5 --- /dev/null +++ b/lib/src/worktree.dart @@ -0,0 +1,54 @@ +import 'dart:ffi'; +import 'bindings/libgit2_bindings.dart'; +import 'bindings/worktree.dart' as bindings; +import 'repository.dart'; + +class Worktree { + /// Initializes a new instance of [Worktree] class by creating new worktree + /// with provided [Repository] object worktree [name] and [path]. + /// + /// Should be freed with `free()` to release allocated memory. + /// + /// Throws a [LibGit2Error] if error occured. + Worktree.create({ + required Repository repo, + required String name, + required String path, + }) { + _worktreePointer = bindings.create(repo.pointer, name, path); + } + + /// Initializes a new instance of [Worktree] class by looking up existing worktree + /// with provided [Repository] object and worktree [name]. + /// + /// Should be freed with `free()` to release allocated memory. + /// + /// Throws a [LibGit2Error] if error occured. + Worktree.lookup(Repository repo, String name) { + _worktreePointer = bindings.lookup(repo.pointer, name); + } + + /// Pointer to memory address for allocated branch object. + late final Pointer _worktreePointer; + + /// Returns list of names of linked working trees. + /// + /// Throws a [LibGit2Error] if error occured. + static List list(Repository repo) => bindings.list(repo.pointer); + + /// Returns the name of the worktree. + String get name => bindings.name(_worktreePointer); + + /// Returns the filesystem path for the worktree. + String get path => bindings.path(_worktreePointer); + + /// Prunes working tree. + /// + /// Prune the working tree, that is remove the git data structures on disk. + /// + /// Throws a [LibGit2Error] if error occured. + void prune() => bindings.prune(_worktreePointer); + + /// Releases memory allocated for worktree object. + void free() => bindings.free(_worktreePointer); +} diff --git a/test/worktree_test.dart b/test/worktree_test.dart new file mode 100644 index 0000000..c31b914 --- /dev/null +++ b/test/worktree_test.dart @@ -0,0 +1,73 @@ +import 'dart:io'; +import 'package:test/test.dart'; +import 'package:libgit2dart/libgit2dart.dart'; +import 'helpers/util.dart'; + +void main() { + late Repository repo; + final tmpDir = '${Directory.systemTemp.path}/worktree_testrepo/'; + final worktreeDir = '${Directory.systemTemp.path}/worktree'; + + setUp(() async { + if (await Directory(tmpDir).exists()) { + await Directory(tmpDir).delete(recursive: true); + } + + if (await Directory(worktreeDir).exists()) { + await Directory(worktreeDir).delete(recursive: true); + } + + await copyRepo( + from: Directory('test/assets/testrepo/'), + to: await Directory(tmpDir).create(), + ); + repo = Repository.open(tmpDir); + }); + + tearDown(() async { + repo.free(); + await Directory(tmpDir).delete(recursive: true); + if (await Directory(worktreeDir).exists()) { + await Directory(worktreeDir).delete(recursive: true); + } + }); + + group('Worktree', () { + test('successfully creates worktree at provided path', () { + const worktreeName = 'worktree'; + expect(Worktree.list(repo), []); + + final worktree = Worktree.create( + repo: repo, + name: worktreeName, + path: worktreeDir, + ); + + expect(Worktree.list(repo), [worktreeName]); + expect(repo.branches.list(), contains(worktreeName)); + expect(worktree.name, worktreeName); + expect(worktree.path, worktreeDir); + expect(File('$worktreeDir/.git').existsSync(), true); + + worktree.free(); + }); + + test('successfully prunes worktree', () { + const worktreeName = 'worktree'; + expect(Worktree.list(repo), []); + + final worktree = Worktree.create( + repo: repo, + name: worktreeName, + path: worktreeDir, + ); + expect(Worktree.list(repo), [worktreeName]); + + Directory(worktreeDir).deleteSync(recursive: true); + worktree.prune(); + expect(Worktree.list(repo), []); + + worktree.free(); + }); + }); +}