From 7b8dfcc1afa9b4b36ac158ffce9fde4e314f49fa Mon Sep 17 00:00:00 2001 From: Aleksey Kulikov Date: Mon, 20 Sep 2021 15:52:04 +0300 Subject: [PATCH] feat(config): add api for config entry --- example/config_example.dart | 8 ++-- lib/src/bindings/config.dart | 35 +++++++-------- lib/src/config.dart | 87 ++++++++++++++++++++++++++++++++---- lib/src/git_types.dart | 48 ++++++++++++++++++++ test/config_test.dart | 24 +++++----- test/repository_test.dart | 6 ++- 6 files changed, 163 insertions(+), 45 deletions(-) diff --git a/example/config_example.dart b/example/config_example.dart index 05dd29e..b394ba0 100644 --- a/example/config_example.dart +++ b/example/config_example.dart @@ -13,8 +13,8 @@ void main() async { final config = Config.open(); print('All entries of system/global config:'); - for (final entry in config.variables.entries) { - print('${entry.key}: ${entry.value}'); + for (final entry in config) { + print('${entry.name}: ${entry.value}'); } // free() should be called on object to free memory when done. config.free(); @@ -25,8 +25,8 @@ void main() async { final repoConfig = Config.open('$tmpDir/.git/config'); print('\nAll entries of repo config:'); - for (final entry in repoConfig.variables.entries) { - print('${entry.key}: ${entry.value}'); + for (final entry in repoConfig) { + print('${entry.name}: ${entry.value}'); } // Set value of config variable diff --git a/lib/src/bindings/config.dart b/lib/src/bindings/config.dart index 6de77fa..fab8516 100644 --- a/lib/src/bindings/config.dart +++ b/lib/src/bindings/config.dart @@ -131,10 +131,10 @@ Pointer snapshot(Pointer config) { } } -/// Get the value of a config variable. +/// Get the config entry of a config variable. /// /// Throws a [LibGit2Error] if error occured. -String getValue(Pointer cfg, String variable) { +Pointer getEntry(Pointer cfg, String variable) { final out = calloc>(); final name = variable.toNativeUtf8().cast(); final error = libgit2.git_config_get_entry(out, cfg, name); @@ -144,7 +144,7 @@ String getValue(Pointer cfg, String variable) { if (error < 0) { throw LibGit2Error(libgit2.git_error_last()); } else { - return out.value.ref.value.cast().toDartString(); + return out.value; } } @@ -197,23 +197,10 @@ void setString(Pointer cfg, String variable, String value) { } /// Iterate over all the config variables. -Map getEntries(Pointer cfg) { - final iterator = calloc>(); - final entry = calloc>(); - libgit2.git_config_iterator_new(iterator, cfg); - var error = 0; - final entries = {}; - - while (error != -31) { - error = libgit2.git_config_next(entry, iterator.value); - entries[entry.value.ref.name.cast().toDartString()] = - entry.value.ref.value.cast().toDartString(); - } - - calloc.free(iterator); - calloc.free(entry); - - return entries; +Pointer iterator(Pointer cfg) { + final out = calloc>(); + libgit2.git_config_iterator_new(out, cfg); + return out.value; } /// Delete a config variable from the config file with the highest level @@ -298,5 +285,13 @@ void deleteMultivar(Pointer cfg, String variable, String regexp) { calloc.free(regexpC); } +/// Free a config iterator. +void iteratorFree(Pointer iter) => + libgit2.git_config_iterator_free(iter); + +/// Free a config entry. +void entryFree(Pointer entry) => + libgit2.git_config_entry_free(entry); + /// Free the configuration and its associated memory and files. void free(Pointer cfg) => libgit2.git_config_free(cfg); diff --git a/lib/src/config.dart b/lib/src/config.dart index 8bf72fc..21cd0b5 100644 --- a/lib/src/config.dart +++ b/lib/src/config.dart @@ -1,10 +1,13 @@ +import 'dart:collection'; import 'dart:ffi'; import 'dart:io'; +import 'package:ffi/ffi.dart'; import 'bindings/libgit2_bindings.dart'; import 'bindings/config.dart' as bindings; +import 'git_types.dart'; import 'util.dart'; -class Config { +class Config with IterableMixin { /// Initializes a new instance of [Config] class from provided /// pointer to config object in memory. /// @@ -89,18 +92,15 @@ class Config { /// Throws a [LibGit2Error] if error occured. Config get snapshot => Config(bindings.snapshot(_configPointer)); - /// Returns map of all the config variables and their values. - Map get variables => bindings.getEntries(_configPointer); - - /// Returns the value of config [variable]. - String operator [](String variable) => - bindings.getValue(_configPointer, variable); + /// Returns the [ConfigEntry] of a [variable]. + ConfigEntry operator [](String variable) => + ConfigEntry(bindings.getEntry(_configPointer, variable)); /// Sets the [value] of config [variable]. void operator []=(String variable, dynamic value) { - if (value.runtimeType == bool) { + if (value is bool) { bindings.setBool(_configPointer, variable, value); - } else if (value.runtimeType == int) { + } else if (value is int) { bindings.setInt(_configPointer, variable, value); } else { bindings.setString(_configPointer, variable, value); @@ -141,4 +141,73 @@ class Config { /// Releases memory allocated for config object. void free() => bindings.free(_configPointer); + + @override + Iterator get iterator => + ConfigIterator(bindings.iterator(_configPointer)); +} + +class ConfigEntry { + ConfigEntry(this._configEntryPointer); + + /// Pointer to memory address for allocated config entry object. + final Pointer _configEntryPointer; + + /// Returns name of the entry (normalised). + String get name => _configEntryPointer.ref.name.cast().toDartString(); + + /// Returns value of the entry. + String get value => _configEntryPointer.ref.value.cast().toDartString(); + + /// Returns depth of includes where this variable was found + int get includeDepth => _configEntryPointer.ref.include_depth; + + /// Returns which config file this was found in. + GitConfigLevel get level { + late GitConfigLevel result; + for (var level in GitConfigLevel.values) { + if (_configEntryPointer.ref.level == level.value) { + result = level; + break; + } + } + return result; + } + + /// Releases memory allocated for config entry object. + void free() => bindings.entryFree(_configEntryPointer); + + @override + String toString() { + return 'ConfigEntry{name: $name, value: $value, includeDepth: $includeDepth, level: $level}'; + } +} + +class ConfigIterator implements Iterator { + ConfigIterator(this._iteratorPointer); + + /// Pointer to memory address for allocated config iterator. + final Pointer _iteratorPointer; + + ConfigEntry? _currentEntry; + int error = 0; + final entry = calloc>(); + + @override + ConfigEntry get current => _currentEntry!; + + @override + bool moveNext() { + if (error < 0) { + return false; + } else { + error = libgit2.git_config_next(entry, _iteratorPointer); + if (error != -31) { + _currentEntry = ConfigEntry(entry.value); + return true; + } else { + return false; + } + } + } } diff --git a/lib/src/git_types.dart b/lib/src/git_types.dart index 0290d6f..60d04ca 100644 --- a/lib/src/git_types.dart +++ b/lib/src/git_types.dart @@ -1072,3 +1072,51 @@ class GitApplyLocation { @override String toString() => 'GitApplyLocation.$_name'; } + +/// Priority level of a config file. +/// These priority levels correspond to the natural escalation logic +/// (from higher to lower) when searching for config entries in git. +class GitConfigLevel { + const GitConfigLevel._(this._value, this._name); + final int _value; + final String _name; + + /// System-wide on Windows, for compatibility with portable git. + static const programData = GitConfigLevel._(1, 'programData'); + + /// System-wide configuration file; /etc/gitconfig on Linux systems. + static const system = GitConfigLevel._(2, 'system'); + + /// XDG compatible configuration file; typically ~/.config/git/config + static const xdg = GitConfigLevel._(3, 'xdg'); + + /// User-specific configuration file (also called Global configuration + /// file); typically ~/.gitconfig + static const global = GitConfigLevel._(4, 'global'); + + /// Repository specific configuration file; $WORK_DIR/.git/config on + /// non-bare repos. + static const local = GitConfigLevel._(5, 'local'); + + /// Application specific configuration file; freely defined by applications. + static const app = GitConfigLevel._(6, 'app'); + + /// Represents the highest level available config file (i.e. the most + /// specific config file available that actually is loaded). + static const highest = GitConfigLevel._(-1, 'highest'); + + static const List values = [ + programData, + system, + xdg, + global, + local, + app, + highest, + ]; + + int get value => _value; + + @override + String toString() => 'GitConfigLevel.$_name'; +} diff --git a/test/config_test.dart b/test/config_test.dart index d51531f..be6fe73 100644 --- a/test/config_test.dart +++ b/test/config_test.dart @@ -33,13 +33,17 @@ void main() { expect(config, isA()); }); - test('returns map with variables and values', () { - expect(config.variables['remote.origin.url'], equals('someurl')); + test('returns config entries and their values', () { + expect(config.length, 5); + expect(config.last.name, 'remote.origin.url'); + expect(config.last.value, 'someurl'); + expect(config.last.includeDepth, 0); + expect(config.last.level, GitConfigLevel.local); }); group('get value', () { test('returns value of variable', () { - expect(config['core.bare'], equals('false')); + expect(config['core.bare'].value, 'false'); }); test('throws when variable isn\'t found', () { @@ -53,25 +57,25 @@ void main() { group('set value', () { test('sets boolean value for provided variable', () { config['core.bare'] = true; - expect(config['core.bare'], equals('true')); + expect(config['core.bare'].value, 'true'); }); test('sets integer value for provided variable', () { config['core.repositoryformatversion'] = 1; - expect(config['core.repositoryformatversion'], equals('1')); + expect(config['core.repositoryformatversion'].value, '1'); }); test('sets string value for provided variable', () { config['remote.origin.url'] = 'updated'; - expect(config['remote.origin.url'], equals('updated')); + expect(config['remote.origin.url'].value, 'updated'); }); }); group('delete', () { test('successfully deletes entry', () { - expect(config['core.bare'], equals('false')); + expect(config['core.bare'].value, 'false'); config.delete('core.bare'); - expect(config.variables['core.bare'], isNull); + expect(() => config['core.bare'], throwsA(isA())); }); test('throws on deleting non existing variable', () { @@ -101,7 +105,7 @@ void main() { }); test('returns empty list if multivar not found', () { - expect(config.multivar('not.there'), equals([])); + expect(config.multivar('not.there'), []); }); }); @@ -119,7 +123,7 @@ void main() { expect(multivarValues, isNot(contains('default-proxy'))); expect(multivarValues, isNot(contains('proxy-command for kernel.org'))); expect(multivarValues, contains('updated')); - expect(multivarValues.length, equals(2)); + expect(multivarValues.length, 2); }); }); diff --git a/test/repository_test.dart b/test/repository_test.dart index 27d6b84..7a047c9 100644 --- a/test/repository_test.dart +++ b/test/repository_test.dart @@ -173,8 +173,10 @@ void main() { test('returns config for repository', () { final config = repo.config; - expect(config['remote.origin.url'], - 'git://github.com/SkinnyMind/libgit2dart.git'); + expect( + config['remote.origin.url'].value, + 'git://github.com/SkinnyMind/libgit2dart.git', + ); config.free(); });