From 07f635739ec77c0449dd11317dfdf3705bbe4962 Mon Sep 17 00:00:00 2001 From: Guru Prathosh <86939870+iamprathosh@users.noreply.github.com> Date: Sat, 11 Jan 2025 19:41:47 +0530 Subject: [PATCH] Add GIT integration Fixes #502 Implement GIT integration to push/pull collections with version history. * **Add GIT Services**: Create `lib/services/git_services.dart` to handle push/export and pull/import functionalities. * **Settings Page**: Modify `lib/screens/settings_page.dart` to include GIT configuration settings (Token, Repository) and add a dialog for GIT settings. * **Collection Pane**: Update `lib/screens/home_page/collection_pane.dart` to add sync buttons for GIT push and pull actions. * **Collection Providers**: Modify `lib/providers/collection_providers.dart` to add methods for handling GIT push and pull actions. * **Constants**: Update `lib/consts.dart` to add constants for GIT settings. * **Settings Model**: Modify `lib/models/settings_model.dart` to include fields for GIT settings. * **Hive Services**: Update `lib/services/hive_services.dart` to add methods for handling GIT data storage and retrieval. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/foss42/apidash/issues/502?shareId=XXXX-XXXX-XXXX-XXXX). --- lib/consts.dart | 4 ++ lib/models/settings_model.dart | 22 ++++++- lib/providers/collection_providers.dart | 21 ++++++- lib/screens/home_page/collection_pane.dart | 23 +++++++ lib/screens/settings_page.dart | 73 ++++++++++++++++++++++ lib/services/git_services.dart | 53 ++++++++++++++++ lib/services/hive_services.dart | 37 +++++++++++ 7 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 lib/services/git_services.dart diff --git a/lib/consts.dart b/lib/consts.dart index 89fe61fb6..1b733d7ab 100644 --- a/lib/consts.dart +++ b/lib/consts.dart @@ -467,3 +467,7 @@ const kMsgNoContent = "No content"; const kMsgUnknowContentType = "Unknown Response Content-Type"; // Workspace Selector const kMsgSelectWorkspace = "Create your workspace"; + +// GIT settings constants +const kGitToken = "gitToken"; +const kGitRepository = "gitRepository"; diff --git a/lib/models/settings_model.dart b/lib/models/settings_model.dart index 74bc2ff9c..348d41813 100644 --- a/lib/models/settings_model.dart +++ b/lib/models/settings_model.dart @@ -17,6 +17,8 @@ class SettingsModel { this.historyRetentionPeriod = HistoryRetentionPeriod.oneWeek, this.workspaceFolderPath, this.isSSLDisabled = false, + this.gitToken, + this.gitRepository, }); final bool isDark; @@ -31,6 +33,8 @@ class SettingsModel { final HistoryRetentionPeriod historyRetentionPeriod; final String? workspaceFolderPath; final bool isSSLDisabled; + final String? gitToken; + final String? gitRepository; SettingsModel copyWith({ bool? isDark, @@ -45,6 +49,8 @@ class SettingsModel { HistoryRetentionPeriod? historyRetentionPeriod, String? workspaceFolderPath, bool? isSSLDisabled, + String? gitToken, + String? gitRepository, }) { return SettingsModel( isDark: isDark ?? this.isDark, @@ -61,6 +67,8 @@ class SettingsModel { historyRetentionPeriod ?? this.historyRetentionPeriod, workspaceFolderPath: workspaceFolderPath ?? this.workspaceFolderPath, isSSLDisabled: isSSLDisabled ?? this.isSSLDisabled, + gitToken: gitToken ?? this.gitToken, + gitRepository: gitRepository ?? this.gitRepository, ); } @@ -80,6 +88,8 @@ class SettingsModel { historyRetentionPeriod: historyRetentionPeriod, workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, + gitToken: gitToken, + gitRepository: gitRepository, ); } @@ -134,6 +144,8 @@ class SettingsModel { } final workspaceFolderPath = data["workspaceFolderPath"] as String?; final isSSLDisabled = data["isSSLDisabled"] as bool?; + final gitToken = data["gitToken"] as String?; + final gitRepository = data["gitRepository"] as String?; const sm = SettingsModel(); @@ -151,6 +163,8 @@ class SettingsModel { historyRetentionPeriod ?? HistoryRetentionPeriod.oneWeek, workspaceFolderPath: workspaceFolderPath, isSSLDisabled: isSSLDisabled, + gitToken: gitToken, + gitRepository: gitRepository, ); } @@ -170,6 +184,8 @@ class SettingsModel { "historyRetentionPeriod": historyRetentionPeriod.name, "workspaceFolderPath": workspaceFolderPath, "isSSLDisabled": isSSLDisabled, + "gitToken": gitToken, + "gitRepository": gitRepository, }; } @@ -194,7 +210,9 @@ class SettingsModel { other.activeEnvironmentId == activeEnvironmentId && other.historyRetentionPeriod == historyRetentionPeriod && other.workspaceFolderPath == workspaceFolderPath && - other.isSSLDisabled == isSSLDisabled; + other.isSSLDisabled == isSSLDisabled && + other.gitToken == gitToken && + other.gitRepository == gitRepository; } @override @@ -213,6 +231,8 @@ class SettingsModel { historyRetentionPeriod, workspaceFolderPath, isSSLDisabled, + gitToken, + gitRepository, ); } } diff --git a/lib/providers/collection_providers.dart b/lib/providers/collection_providers.dart index c3f50291b..557c9b590 100644 --- a/lib/providers/collection_providers.dart +++ b/lib/providers/collection_providers.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:apidash/consts.dart'; import 'providers.dart'; import '../models/models.dart'; -import '../services/services.dart' show hiveHandler, HiveHandler; +import '../services/services.dart' show hiveHandler, HiveHandler, GitService; import '../utils/utils.dart' show getNewUuid, collectionToHAR, substituteHttpRequestModel; @@ -415,4 +415,23 @@ class CollectionStateNotifier activeEnvId, ); } + + Future pushToGit() async { + final settings = ref.read(settingsProvider); + final gitService = GitService( + repositoryUrl: settings.gitRepository!, + token: settings.gitToken!, + ); + await gitService.pushData(); + } + + Future pullFromGit() async { + final settings = ref.read(settingsProvider); + final gitService = GitService( + repositoryUrl: settings.gitRepository!, + token: settings.gitToken!, + ); + await gitService.pullData(); + loadData(); + } } diff --git a/lib/screens/home_page/collection_pane.dart b/lib/screens/home_page/collection_pane.dart index bfac27a89..c2c89f307 100644 --- a/lib/screens/home_page/collection_pane.dart +++ b/lib/screens/home_page/collection_pane.dart @@ -7,6 +7,7 @@ import 'package:apidash/widgets/widgets.dart'; import 'package:apidash/models/models.dart'; import 'package:apidash/consts.dart'; import '../common_widgets/common_widgets.dart'; +import 'package:apidash/services/git_services.dart'; class CollectionPane extends ConsumerWidget { const CollectionPane({ @@ -45,6 +46,28 @@ class CollectionPane extends ConsumerWidget { }, ), kVSpacer10, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon(Icons.cloud_upload), + onPressed: () async { + await ref + .read(collectionStateNotifierProvider.notifier) + .pushToGit(); + }, + ), + IconButton( + icon: const Icon(Icons.cloud_download), + onPressed: () async { + await ref + .read(collectionStateNotifierProvider.notifier) + .pullFromGit(); + }, + ), + ], + ), + kVSpacer10, const Expanded( child: RequestList(), ), diff --git a/lib/screens/settings_page.dart b/lib/screens/settings_page.dart index eb4b60088..38b3e28a4 100644 --- a/lib/screens/settings_page.dart +++ b/lib/screens/settings_page.dart @@ -232,6 +232,23 @@ class SettingsPage extends ConsumerWidget { showAboutAppDialog(context); }, ), + ListTile( + hoverColor: kColorTransparent, + title: const Text('GIT Configuration'), + subtitle: const Text( + 'Configure GIT settings to enable push/pull functionality.'), + trailing: IconButton( + icon: const Icon(Icons.settings), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return GitSettingsDialog(); + }, + ); + }, + ), + ), kVSpacer20, ], ), @@ -240,3 +257,59 @@ class SettingsPage extends ConsumerWidget { ); } } + +class GitSettingsDialog extends ConsumerStatefulWidget { + @override + _GitSettingsDialogState createState() => _GitSettingsDialogState(); +} + +class _GitSettingsDialogState extends ConsumerState { + final TextEditingController _tokenController = TextEditingController(); + final TextEditingController _repositoryController = TextEditingController(); + + @override + void initState() { + super.initState(); + final settings = ref.read(settingsProvider); + _tokenController.text = settings.gitToken ?? ''; + _repositoryController.text = settings.gitRepository ?? ''; + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('GIT Settings'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _tokenController, + decoration: const InputDecoration(labelText: 'GIT Token'), + ), + TextField( + controller: _repositoryController, + decoration: const InputDecoration(labelText: 'GIT Repository'), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + ref.read(settingsProvider.notifier).update( + gitToken: _tokenController.text, + gitRepository: _repositoryController.text, + ); + Navigator.of(context).pop(); + }, + child: const Text('Save'), + ), + ], + ); + } +} diff --git a/lib/services/git_services.dart b/lib/services/git_services.dart new file mode 100644 index 000000000..1efa003c6 --- /dev/null +++ b/lib/services/git_services.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; +import 'package:git/git.dart'; +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:apidash/consts.dart'; + +class GitService { + final String repositoryUrl; + final String token; + final String branch; + + GitService({ + required this.repositoryUrl, + required this.token, + this.branch = 'main', + }); + + Future pushData() async { + final directory = await getApplicationDocumentsDirectory(); + final repoDir = '${directory.path}/apidash_repo'; + + final gitDir = await GitDir.fromExisting(repoDir, allowSubdirectory: true); + await gitDir.runCommand(['pull', 'origin', branch]); + + final box = await Hive.openBox(kDataBox); + final data = box.toMap(); + final jsonData = jsonEncode(data); + + final file = File('$repoDir/collections.json'); + await file.writeAsString(jsonData); + + await gitDir.runCommand(['add', 'collections.json']); + await gitDir.runCommand(['commit', '-m', 'Update collections']); + await gitDir.runCommand(['push', 'origin', branch]); + } + + Future pullData() async { + final directory = await getApplicationDocumentsDirectory(); + final repoDir = '${directory.path}/apidash_repo'; + + final gitDir = await GitDir.fromExisting(repoDir, allowSubdirectory: true); + await gitDir.runCommand(['pull', 'origin', branch]); + + final file = File('$repoDir/collections.json'); + if (await file.exists()) { + final jsonData = await file.readAsString(); + final data = jsonDecode(jsonData); + + final box = await Hive.openBox(kDataBox); + await box.putAll(data); + } + } +} diff --git a/lib/services/hive_services.dart b/lib/services/hive_services.dart index 09ac8489b..2bf2b43c3 100644 --- a/lib/services/hive_services.dart +++ b/lib/services/hive_services.dart @@ -1,5 +1,8 @@ import 'package:flutter/foundation.dart'; import 'package:hive_flutter/hive_flutter.dart'; +import 'dart:convert'; +import 'package:git/git.dart'; +import 'package:path_provider/path_provider.dart'; const String kDataBox = "apidash-data"; const String kKeyDataBoxIds = "ids"; @@ -167,4 +170,38 @@ class HiveHandler { } } } + + Future pushDataToGit(String repositoryUrl, String token) async { + final directory = await getApplicationDocumentsDirectory(); + final repoDir = '${directory.path}/apidash_repo'; + + final gitDir = await GitDir.fromExisting(repoDir, allowSubdirectory: true); + await gitDir.runCommand(['pull', 'origin', 'main']); + + final data = dataBox.toMap(); + final jsonData = jsonEncode(data); + + final file = File('$repoDir/collections.json'); + await file.writeAsString(jsonData); + + await gitDir.runCommand(['add', 'collections.json']); + await gitDir.runCommand(['commit', '-m', 'Update collections']); + await gitDir.runCommand(['push', 'origin', 'main']); + } + + Future pullDataFromGit(String repositoryUrl, String token) async { + final directory = await getApplicationDocumentsDirectory(); + final repoDir = '${directory.path}/apidash_repo'; + + final gitDir = await GitDir.fromExisting(repoDir, allowSubdirectory: true); + await gitDir.runCommand(['pull', 'origin', 'main']); + + final file = File('$repoDir/collections.json'); + if (await file.exists()) { + final jsonData = await file.readAsString(); + final data = jsonDecode(jsonData); + + await dataBox.putAll(data); + } + } }