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 'commit.dart'; import 'config.dart'; import 'index.dart'; import 'odb.dart'; import 'oid.dart'; import 'reference.dart'; import 'revwalk.dart'; import 'revparse.dart'; import 'blob.dart'; import 'git_types.dart'; import 'signature.dart'; import 'tag.dart'; import 'util.dart'; class Repository { /// Initializes a new instance of the [Repository] class from provided /// pointer to repository object in memory. /// /// Should be freed with `free()` to release allocated memory. Repository(this._repoPointer) { libgit2.git_libgit2_init(); } /// Initializes a new instance of the [Repository] class by creating a new /// Git repository in the given folder. /// /// Should be freed with `free()` to release allocated memory. /// /// Throws a [LibGit2Error] if error occured. Repository.init(String path, {bool isBare = false}) { libgit2.git_libgit2_init(); _repoPointer = bindings.init(path, isBare); } /// Initializes a new instance of the [Repository] class. /// For a standard repository, [path] should either point to the `.git` folder /// or to the working directory. For a bare repository, [path] should directly /// point to the repository folder. /// /// Should be freed with `free()` to release allocated memory. /// /// Throws a [LibGit2Error] if error occured. Repository.open(String path) { libgit2.git_libgit2_init(); _repoPointer = bindings.open(path); } late final Pointer _repoPointer; /// Pointer to memory address for allocated repository object. Pointer get pointer => _repoPointer; /// Look for a git repository and return its path. The lookup start from [startPath] /// and walk across parent directories if nothing has been found. The lookup ends when /// the first repository is found, or when reaching a directory referenced in [ceilingDirs]. /// /// The method will automatically detect if the repository is bare (if there is a repository). /// /// Throws a [LibGit2Error] if error occured. static String discover(String startPath, [String ceilingDirs = '']) { return bindings.discover(startPath, ceilingDirs); } /// Returns path to the `.git` folder for normal repositories /// or path to the repository itself for bare repositories. String get path => bindings.path(_repoPointer); /// Returns the path of the shared common directory for this repository. /// /// If the repository is bare, it is the root directory for the repository. /// If the repository is a worktree, it is the parent repo's `.git` folder. /// Otherwise, it is the `.git` folder. String get commonDir => bindings.commonDir(_repoPointer); /// Returns the currently active namespace for this repository. /// /// If there is no namespace, or the namespace is not a valid utf8 string, /// empty string is returned. String get namespace => bindings.getNamespace(_repoPointer); /// Sets the active namespace for this repository. /// /// This namespace affects all reference operations for the repo. See `man gitnamespaces` /// /// The [namespace] should not include the refs folder, e.g. to namespace all references /// under refs/namespaces/foo/, use foo as the namespace. /// /// Pass null to unset. /// /// Throws a [LibGit2Error] if error occured. void setNamespace(String? namespace) { bindings.setNamespace(_repoPointer, namespace); } /// Checks whether this repository is a bare repository or not. bool get isBare => bindings.isBare(_repoPointer); /// Check if a repository is empty. /// /// An empty repository has just been initialized and contains no references /// apart from HEAD, which must be pointing to the unborn master branch. /// /// Throws a [LibGit2Error] if repository is corrupted. bool get isEmpty => bindings.isEmpty(_repoPointer); /// Checks if a repository's HEAD is detached. /// /// A repository's HEAD is detached when it points directly to a commit instead of a branch. /// /// Throws a [LibGit2Error] if error occured. bool get isHeadDetached { return bindings.isHeadDetached(_repoPointer); } /// Makes the repository HEAD point to the specified reference or commit. /// /// If the provided [target] points to a Tree or a Blob, the HEAD is unaltered. /// /// If the provided [target] points to a branch, the HEAD will point to that branch, /// staying attached, or become attached if it isn't yet. /// /// If the branch doesn't exist yet, the HEAD will be attached to an unborn branch. /// /// Otherwise, the HEAD will be detached and will directly point to the Commit. /// /// Throws a [LibGit2Error] if error occured. void setHead(String target) { late final Oid oid; if (isValidShaHex(target)) { oid = Oid.fromSHA(this, target); bindings.setHeadDetached(_repoPointer, oid.pointer); } else { bindings.setHead(_repoPointer, target); } } /// Checks if the current branch is unborn. /// /// An unborn branch is one named from HEAD but which doesn't exist in the refs namespace, /// because it doesn't have any commit to point to. /// /// Throws a [LibGit2Error] if error occured. bool get isBranchUnborn { return bindings.isBranchUnborn(_repoPointer); } /// Sets the identity to be used for writing reflogs. /// /// If both are set, this name and email will be used to write to the reflog. /// Pass null to unset. When unset, the identity will be taken from the repository's configuration. void setIdentity({required String? name, required String? email}) { bindings.setIdentity(_repoPointer, name, email); } /// Returns the configured identity to use for reflogs. Map get identity => bindings.identity(_repoPointer); /// Checks if the repository was a shallow clone. bool get isShallow => bindings.isShallow(_repoPointer); /// Checks if a repository is a linked work tree. bool get isWorktree => bindings.isWorktree(_repoPointer); /// Retrieves git's prepared message. /// /// Operations such as git revert/cherry-pick/merge with the -n option /// stop just short of creating a commit with the changes and save their /// prepared message in .git/MERGE_MSG so the next git-commit execution /// can present it to the user for them to amend if they wish. /// /// Use this function to get the contents of this file. /// Don't forget to remove the file with [removeMessage] after you create the commit. /// /// Throws a [LibGit2Error] if error occured. String get message => bindings.message(_repoPointer); /// Removes git's prepared message. void removeMessage() => bindings.removeMessage(_repoPointer); /// Returns the status of a git repository - ie, whether an operation /// (merge, cherry-pick, etc) is in progress. // git_repository_state_t from libgit2_bindings.dart represents possible states int get state => bindings.state(_repoPointer); /// Removes all the metadata associated with an ongoing command like /// merge, revert, cherry-pick, etc. For example: MERGE_HEAD, MERGE_MSG, etc. /// /// Throws a [LibGit2Error] if error occured. void stateCleanup() => bindings.stateCleanup(_repoPointer); /// Returns the path of the working directory for this repository. /// /// If the repository is bare, this function will always return empty string. String get workdir => bindings.workdir(_repoPointer); /// Sets the path to the working directory for this repository. /// /// The working directory doesn't need to be the same one that contains the /// `.git` folder for this repository. /// /// If this repository is bare, setting its working directory will turn it into a /// normal repository, capable of performing all the common workdir operations /// (checkout, status, index manipulation, etc). /// /// Throws a [LibGit2Error] if error occured. void setWorkdir(String path, [bool updateGitlink = false]) { bindings.setWorkdir(_repoPointer, path, updateGitlink); } /// Releases memory allocated for repository object. void free() => bindings.free(_repoPointer); /// Returns the configuration file for this repository. /// /// If a configuration file has not been set, the default config set for the repository /// will be returned, including global and system configurations (if they are available). /// /// The configuration file must be freed once it's no longer being used by the user. /// /// Throws a [LibGit2Error] if error occured. Config get config => Config(bindings.config(_repoPointer)); /// Returns a snapshot of the repository's configuration. /// /// Convenience function to take a snapshot from the repository's configuration. /// The contents of this snapshot will not change, even if the underlying config files are modified. /// /// The configuration file must be freed once it's no longer being used by the user. /// /// Throws a [LibGit2Error] if error occured. Config get configSnapshot => Config(bindings.configSnapshot(_repoPointer)); /// Returns [Reference] object pointing to repository head. /// /// Must be freed once it's no longer being used. Reference get head => Reference(_repoPointer, bindings.head(_repoPointer)); /// Returns [References] object. References get references => References(this); /// Creates a new reference. /// /// The reference will be created in the repository and written to the disk. /// The generated [Reference] object must be freed by the user. /// /// Valid reference names must follow one of two patterns: /// /// Top-level names must contain only capital letters and underscores, and must begin and end /// with a letter. (e.g. "HEAD", "ORIG_HEAD"). /// Names prefixed with "refs/" can be almost anything. You must avoid the characters /// '~', '^', ':', '\', '?', '[', and '*', and the sequences ".." and "@{" which have /// special meaning to revparse. /// Throws a [LibGit2Error] if a reference already exists with the given name /// unless force is true, in which case it will be overwritten. /// /// The message for the reflog will be ignored if the reference does not belong in the /// standard set (HEAD, branches and remote-tracking branches) and it does not have a reflog. Reference createReference({ required String name, required Object target, bool force = false, String? logMessage, }) { late final Oid oid; late final bool isDirect; if (target is Oid) { oid = target; isDirect = true; } else if (isValidShaHex(target as String)) { oid = Oid.fromSHA(this, target); isDirect = true; } else { isDirect = false; } if (isDirect) { return Reference.createDirect( repo: this, name: name, oid: oid.pointer, force: force, logMessage: logMessage, ); } else { return Reference.createSymbolic( repo: this, name: name, target: target as String, force: force, logMessage: logMessage, ); } } /// Returns [Index] file for this repository. /// /// Must be freed once it's no longer being used. Index get index => Index(bindings.index(_repoPointer)); /// Returns [Odb] for this repository. /// /// ODB Object must be freed once it's no longer being used. /// /// Throws a [LibGit2Error] if error occured. Odb get odb => Odb(bindings.odb(_repoPointer)); /// Looksup git object (commit, tree, blob, tag) for provided [sha] hex string. /// /// Returned object should be explicitly downcasted to one of four of git object types. /// /// ```dart /// final commit = repo['s0m3sh4'] as Commit; /// final tree = repo['s0m3sh4'] as Tree; /// final blob = repo['s0m3sh4'] as Blob; /// final tag = repo['s0m3sh4'] as Tag; /// ``` /// /// Throws [ArgumentError] if provided [sha] is not pointing to commit, tree, blob or tag. Object operator [](String sha) { final oid = Oid.fromSHA(this, sha); final object = object_bindings.lookup( _repoPointer, oid.pointer, GitObject.any.value, ); final type = object_bindings.type(object); if (type == GitObject.commit.value) { return Commit(object.cast()); } else if (type == GitObject.tree.value) { return Tree(object.cast()); } else if (type == GitObject.blob.value) { return Blob(object.cast()); } else if (type == GitObject.tag.value) { return Tag(object.cast()); } else { throw ArgumentError.value( '$sha should be pointing to either commit, tree, blob or a tag'); } } /// Returns the list of commits starting from provided [sha] hex string. /// /// If [sorting] isn't provided default will be used (reverse chronological order, like in git). List log(String sha, [List? sorting]) { final oid = Oid.fromSHA(this, sha); final walker = RevWalk(this); walker.sorting(sorting ?? [GitSort.none]); walker.push(oid); final result = walker.walk(); walker.free(); return result; } /// Finds a single object, as specified by a [spec] revision string. /// See `man gitrevisions`, or https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions /// for information on the syntax accepted. /// /// The returned object should be released when no longer needed. /// /// Throws a [LibGit2Error] if error occured. Commit revParseSingle(String spec) => RevParse.single(this, spec); /// 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 /// for information on the syntax accepted. /// /// In some cases (@{<-n>} or @{upstream}), the expression may point to an /// intermediate reference. When such expressions are being passed in, reference_out will be /// valued as well. /// /// The returned object and reference should be released when no longer needed. /// /// Throws a [LibGit2Error] if error occured. RevParse revParseExt(String spec) => RevParse.ext(this, spec); /// Parses a revision string for from, to, and intent. /// /// See `man gitrevisions` or https://git-scm.com/docs/git-rev-parse.html#_specifying_revisions /// for information on the syntax accepted. /// /// Throws a [LibGit2Error] if error occured. RevSpec revParse(String spec) => RevParse.range(this, spec); /// Finds a merge base between two commits. /// /// Throws a [LibGit2Error] if error occured. Oid mergeBase(String one, String two) { final oidOne = Oid.fromSHA(this, one); final oidTwo = Oid.fromSHA(this, two); return Oid(merge_bindings.mergeBase( _repoPointer, oidOne.pointer, oidTwo.pointer, )); } /// Creates a new blob from a [content] string and writes it to ODB. /// /// Throws a [LibGit2Error] if error occured. Oid createBlob(String content) => Blob.create(this, content); /// Creates a new blob from the file in working directory of a repository and writes /// it to the ODB. Provided [path] should be relative to the working directory. /// /// Throws a [LibGit2Error] if error occured. Oid createBlobFromWorkdir(String relativePath) => Blob.createFromWorkdir(this, relativePath); /// Creates a new blob from the file in filesystem and writes it to the ODB. /// /// Throws a [LibGit2Error] if error occured. Oid createBlobFromDisk(String path) => Blob.createFromDisk(this, path); /// Creates a new tag in the repository from provided Oid object. /// /// A new reference will also be created pointing to this tag object. If force is true /// and a reference already exists with the given name, it'll be replaced. /// /// The message will not be cleaned up. /// /// The tag name will be checked for validity. You must avoid the characters /// '~', '^', ':', '\', '?', '[', and '*', and the sequences ".." and "@{" which have /// special meaning to revparse. /// /// Throws a [LibGit2Error] if error occured. Oid createTag({ required String tagName, required String target, required GitObject targetType, required Signature tagger, required String message, bool force = false, }) { return Tag.create( repository: this, tagName: tagName, target: target, targetType: targetType, tagger: tagger, message: message, force: force); } }