diff --git a/example/config_example.dart b/example/config_example.dart deleted file mode 100644 index fc8e45e..0000000 --- a/example/config_example.dart +++ /dev/null @@ -1,62 +0,0 @@ -// ignore_for_file: avoid_print - -import 'dart:io'; -import 'package:libgit2dart/libgit2dart.dart'; -import '../test/helpers/util.dart'; - -void main() { - // Preparing example repository. - final tmpDir = setupRepo(Directory('test/assets/testrepo/')); - - // Open system + global config file. - final config = Config.open(); - - print('All entries of system/global config:'); - for (final entry in config) { - print('${entry.name}: ${entry.value}'); - } - // free() should be called on object to free memory when done. - config.free(); - - // Open config file at provided path. - // Exception is thrown if file not found. - try { - final repoConfig = Config.open('${tmpDir.path}/.git/config'); - - print('\nAll entries of repo config:'); - for (final entry in repoConfig) { - print('${entry.name}: ${entry.value}'); - } - - // Set value of config variable - repoConfig['core.variable'] = 'value'; - print( - '\nNew value for variable ' - '"core.variable": ${repoConfig['core.variable']}', - ); - - // Delete variable - repoConfig.delete('core.variable'); - - repoConfig.free(); - } catch (e) { - print(e); - } - - // Open global config file if there's one. - // Exception is thrown if file not found. - try { - final globalConfig = Config.global(); - - // Get value of config variable. - final userName = globalConfig['user.name']; - print('\nUser Name from global config: $userName'); - - globalConfig.free(); - } catch (e) { - print('\n$e'); - } - - // Removing example repository. - tmpDir.deleteSync(recursive: true); -} diff --git a/example/example.dart b/example/example.dart new file mode 100644 index 0000000..96a4a77 --- /dev/null +++ b/example/example.dart @@ -0,0 +1,398 @@ +import 'dart:io'; + +import 'package:libgit2dart/libgit2dart.dart'; +import 'package:path/path.dart' as path; + +// These examples are basic emulation of core Git CLI functions demonstrating +// basic libgit2dart API usage. Be advised that they don't have error handling, +// copy with caution. + +void main() { + // Prepare empty directory for repository. + final tmpDir = Directory.systemTemp.createTempSync('example_repo'); + + // Initialize a repository. + final repo = initRepo(tmpDir.path); + + // Setup user name and email. + setupNameAndEmail(repo); + + // Stage untracked file. + const fileName = 'file.txt'; + final filePath = path.join(repo.workdir, fileName); + File(filePath).createSync(); + stageUntracked(repo: repo, filePath: fileName); + + // Stage modified file. + File(filePath).writeAsStringSync('some edit\n'); + stageModified(repo: repo, filePath: fileName); + + // Check repository status. + repoStatus(repo); + + // Commit changes. + commitChanges(repo); + + // View commit history. + viewHistory(repo); + + // View a particular commit. + viewCommit(repo); + + // View changes before commiting. + File(filePath).writeAsStringSync('some changes\n'); + viewChanges(repo); + + // Discard staged changes. + stageModified(repo: repo, filePath: fileName); + discardStaged(repo: repo, filePath: fileName); + + // Discard changes in the working directory. + discardNotStaged(repo: repo, filePath: fileName); + + // Remove tracked file from the index and current working tree. + removeFile(repo: repo, filePath: fileName); + + // Amend the most recent commit. + amendCommit(repo); + + // Create and switch to a new local branch. + createAndSwitchToBranch(repo); + + // List all branches. + listBranches(repo); + + // Merge two branches. + mergeBranches(repo); + + // Delete a branch. + deleteBranch(repo); + + // Add a remote repository. + addRemote(repo); + + // View remote URLs. + viewRemoteUrls(repo); + + // Remove a remote repository. + removeRemote(repo); + + // Clean up. + tmpDir.deleteSync(recursive: true); +} + +/// Initialize a repository at provided path. +/// +/// Similar to `git init`. +Repository initRepo(String path) { + final repo = Repository.init(path: path); + stdout.writeln('Initialized empty Git repository in ${repo.path}'); + return repo; +} + +/// Setup user name and email. +/// +/// Similar to: +/// - `git config --add user.name "User Name"` +/// - `git config --add user.email "user@email.com"` +void setupNameAndEmail(Repository repo) { + final config = repo.config; + config['user.name'] = 'User Name'; + config['user.email'] = 'user@email.com'; + stdout.writeln('\nSetup user name and email:'); + stdout.writeln('user.name=${config['user.name'].value}'); + stdout.writeln('user.email=${config['user.email'].value}'); +} + +/// Stage untracked file. +/// +/// Similar to `git add file.txt` +void stageUntracked({required Repository repo, required String filePath}) { + final index = repo.index; + index.add(filePath); + index.write(); + stdout.writeln('\nStaged previously untracked file $filePath'); +} + +/// Stage modified file. +/// +/// Similar to `git add file.txt` +void stageModified({required Repository repo, required String filePath}) { + final index = repo.index; + index.updateAll([filePath]); + index.write(); + stdout.writeln('\nChanges to $filePath were staged'); +} + +/// Check repository status. +/// +/// Similar to `git status` +void repoStatus(Repository repo) { + stdout.writeln('\nChanges to be committed:'); + for (final file in repo.status.entries) { + if (file.value.contains(GitStatus.indexNew)) { + stdout.writeln('\tnew file: \t${file.key}'); + } + if (file.value.contains(GitStatus.indexModified)) { + stdout.writeln('\tmodified: \t${file.key}'); + } + } +} + +/// Commit changes. +/// +/// Similar to `git commit -m "initial commit"` +void commitChanges(Repository repo) { + final signature = repo.defaultSignature; + const commitMessage = 'initial commit\n'; + + repo.index.write(); + + final oid = Commit.create( + repo: repo, + updateRef: 'HEAD', + author: signature, + committer: signature, + message: commitMessage, + tree: Tree.lookup(repo: repo, oid: repo.index.writeTree()), + parents: [], // root commit doesn't have parents + ); + + stdout.writeln( + '\n[${repo.head.shorthand} (root-commit) ${oid.sha.substring(0, 7)}] ' + '$commitMessage', + ); +} + +/// View commit history. +/// +/// Similar to `git log` +void viewHistory(Repository repo) { + final commits = repo.log(oid: repo.head.target); + + for (final commit in commits) { + stdout.writeln('\ncommit ${commit.oid.sha}'); + stdout.writeln('Author: ${commit.author.name} <${commit.author.email}>'); + stdout.writeln( + 'Date: ${DateTime.fromMillisecondsSinceEpoch(commit.time * 1000)} ' + '${commit.timeOffset}', + ); + stdout.writeln('\n\t${commit.message}'); + } +} + +/// View a particular commit. +/// +/// Similar to `git show aaf8f1e` +void viewCommit(Repository repo) { + final commit = Commit.lookup(repo: repo, oid: repo.head.target); + + stdout.writeln('\ncommit ${commit.oid.sha}'); + stdout.writeln('Author: ${commit.author.name} <${commit.author.email}>'); + stdout.writeln( + 'Date: ${DateTime.fromMillisecondsSinceEpoch(commit.time * 1000)} ' + '${commit.timeOffset}', + ); + stdout.writeln('\n\t${commit.message}'); + + final diff = Diff.treeToTree( + repo: repo, + oldTree: null, + newTree: commit.tree, + ); + stdout.writeln('\n${diff.patch}'); +} + +/// View changes before commiting. +/// +/// Similar to `git diff` +void viewChanges(Repository repo) { + final diff = Diff.indexToWorkdir(repo: repo, index: repo.index); + stdout.writeln('\n${diff.patch}'); +} + +/// Discard staged changes. +/// +/// Similar to `git restore --staged file.txt` +void discardStaged({required Repository repo, required String filePath}) { + repo.resetDefault(oid: repo.head.target, pathspec: [filePath]); + stdout.writeln('Staged changes to $filePath were discarded'); +} + +/// Discard changes in the working directory. +/// +/// Similar to `git restore file.txt` +void discardNotStaged({required Repository repo, required String filePath}) { + final patch = Patch.fromBuffers( + // Current content of modified file. + oldBuffer: File(path.join(repo.workdir, filePath)).readAsStringSync(), + oldBufferPath: filePath, + // Old content of file as found in previously written blob. + newBuffer: Blob.lookup(repo: repo, oid: repo.index[filePath].oid).content, + newBufferPath: filePath, + ); + + // Apply a diff to the repository, making changes in working directory. + Diff.parse(patch.text).apply(repo: repo); + stdout.writeln('\nChanges to $filePath in working directory were discarded.'); +} + +/// Remove tracked files from the index and current working tree. +/// +/// Similar to `git rm file.txt` +void removeFile({required Repository repo, required String filePath}) { + File(path.join(repo.workdir, filePath)).deleteSync(); + repo.index.updateAll([filePath]); + stdout.writeln('\nrm $filePath'); +} + +/// Amend the most recent commit. +/// +/// Similar to `git commit --amend -m "Updated message for the previous commit"` +void amendCommit(Repository repo) { + const commitMessage = "Updated message for the previous commit\n"; + + repo.index.write(); + + final oid = Commit.amend( + repo: repo, + commit: Commit.lookup(repo: repo, oid: repo.head.target), + updateRef: 'HEAD', + message: commitMessage, + tree: Tree.lookup(repo: repo, oid: repo.index.writeTree()), + ); + + stdout.writeln( + '\n[${repo.head.shorthand} ${oid.sha.substring(0, 7)}] ' + '$commitMessage', + ); +} + +/// Create and switch to a new local branch. +/// +/// Similar to `git checkout -b new-branch` +void createAndSwitchToBranch(Repository repo) { + final branch = Branch.create( + repo: repo, + name: 'new-branch', + target: Commit.lookup(repo: repo, oid: repo.head.target), + ); + final fullName = 'refs/heads/${branch.name}'; + Checkout.reference(repo: repo, name: fullName); + repo.setHead(fullName); + stdout.writeln('Switched to a new branch "${repo.head.name}"'); +} + +/// List all branches. +/// +/// Similar to `git branch -a` +void listBranches(Repository repo) { + stdout.writeln(); + + final branches = repo.branches; + for (final branch in branches) { + stdout.writeln( + repo.head.shorthand == branch.name + ? '* ${branch.name}' + : ' ${branch.name}', + ); + } +} + +/// Merge two branches. +/// +/// Example shows the simplest fast-forward merge. In reality you could find +/// the base between commits with [Merge.base], perform analysis for merge with +/// [Merge.analysis] and based on result invoke further needed methods. +/// +/// Similar to `git merge new-branch` +void mergeBranches(Repository repo) { + // Making changes on 'new-branch' + File(path.join(repo.workdir, 'new_branch_file.txt')).createSync(); + + // Committing on 'new-branch' + final signature = repo.defaultSignature; + repo.index.write(); + final newBranchOid = Commit.create( + repo: repo, + updateRef: 'HEAD', + author: signature, + committer: signature, + message: 'commit on new-branch\n', + tree: Tree.lookup(repo: repo, oid: repo.index.writeTree()), + parents: [Commit.lookup(repo: repo, oid: repo.head.target)], + ); + + // Switching to 'master' + Checkout.reference(repo: repo, name: 'refs/heads/master'); + repo.setHead('refs/heads/master'); + + // Merging commit into HEAD and writing results into the working directory. + // Repository is put into a merging state. + Merge.commit( + repo: repo, + commit: AnnotatedCommit.lookup(repo: repo, oid: newBranchOid), + ); + + // Staging merged files. + repo.index.addAll(repo.status.keys.toList()); + + // Committing on 'master' + repo.index.write(); + final parent = Commit.lookup(repo: repo, oid: repo.head.target); + final masterOid = Commit.create( + repo: repo, + updateRef: 'HEAD', + author: signature, + committer: signature, + message: 'commit on new-branch\n', + tree: Tree.lookup(repo: repo, oid: repo.index.writeTree()), + parents: [parent], + ); + + // Clearing up merging state of repository after commit is done + repo.stateCleanup(); + + stdout.writeln( + '\nUpdating ${parent.oid.sha.substring(0, 7)}..' + '${masterOid.sha.substring(0, 7)}', + ); +} + +/// Delete a branch. +/// +/// Similar to `git branch -d new-branch` +void deleteBranch(Repository repo) { + const name = 'new-branch'; + final tip = Branch.lookup(repo: repo, name: name).target.sha; + Branch.delete(repo: repo, name: name); + stdout.writeln('\nDeleted branch $name (was ${tip.substring(0, 7)}).'); +} + +/// Add a remote repository. +/// +/// Similar to `git remote add origin https://some.url` +void addRemote(Repository repo) { + const remoteName = 'origin'; + const url = 'https://some.url'; + Remote.create(repo: repo, name: remoteName, url: url); + Remote.setPushUrl(repo: repo, remote: remoteName, url: url); +} + +/// View remote URLs. +/// +/// Similar to `git remote -v` +void viewRemoteUrls(Repository repo) { + for (final remoteName in repo.remotes) { + final remote = Remote.lookup(repo: repo, name: remoteName); + stdout.writeln('\n${remote.name} ${remote.url} (fetch)'); + stdout.writeln('${remote.name} ${remote.pushUrl} (push)'); + } +} + +/// Remove a remote repository. +/// +/// Similar to `git remote remove origin` +void removeRemote(Repository repo) { + Remote.delete(repo: repo, name: 'origin'); +} diff --git a/example/reference_example.dart b/example/reference_example.dart deleted file mode 100644 index 8607e22..0000000 --- a/example/reference_example.dart +++ /dev/null @@ -1,44 +0,0 @@ -// ignore_for_file: avoid_print - -import 'dart:io'; -import 'package:libgit2dart/libgit2dart.dart'; -import '../test/helpers/util.dart'; - -void main() { - // Preparing example repository. - final tmpDir = setupRepo(Directory('test/assets/testrepo/')); - - final repo = Repository.open(tmpDir.path); - - // Get list of repo's references. - print('Repository references: ${repo.references}'); - - // Get reference by name. - final ref = Reference.lookup(repo: repo, name: 'refs/heads/master'); - - print('Reference SHA hex: ${ref.target.sha}'); - print('Is reference a local branch: ${ref.isBranch}'); - print('Reference full name: ${ref.name}'); - print('Reference shorthand name: ${ref.shorthand}'); - - // Create new reference (direct or symbolic). - final newRef = Reference.create( - repo: repo, - name: 'refs/tags/v1', - target: 'refs/heads/master', - ); - - // Rename reference. - Reference.rename(repo: repo, oldName: 'v1', newName: 'refs/tags/v1.1'); - - // Delete reference. - Reference.delete(repo: repo, name: 'v1.1'); - - // free() should be called on object to free memory when done. - ref.free(); - newRef.free(); - repo.free(); - - // Removing example repository. - tmpDir.deleteSync(recursive: true); -} diff --git a/example/repository_example.dart b/example/repository_example.dart deleted file mode 100644 index 2d6fd55..0000000 --- a/example/repository_example.dart +++ /dev/null @@ -1,22 +0,0 @@ -// ignore_for_file: avoid_print - -import 'dart:io'; - -import 'package:libgit2dart/libgit2dart.dart'; - -void main() { - // Open repository - try { - final repo = Repository.open(Directory.current.path); - - print('Path to git repository: ${repo.path}'); - print('Is repository bare: ${repo.isBare}'); - print('Is repository empty: ${repo.isEmpty}'); - print('Is head detached: ${repo.isHeadDetached}'); - - // free() should be called on object to free memory when done. - repo.free(); - } catch (e) { - print(e); - } -} diff --git a/test/helpers/util.dart b/test/helpers/util.dart index 17d4066..3dddb5e 100644 --- a/test/helpers/util.dart +++ b/test/helpers/util.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:libgit2dart/libgit2dart.dart'; import 'package:path/path.dart' as p; +/// Copies repository at provided [repoDir] into system's temp directory. Directory setupRepo(Directory repoDir) { Libgit2.ownerValidation = false; final tmpDir = Directory.systemTemp.createTempSync('testrepo');