import 'dart:async'; import 'dart:io' show Platform; import 'package:dart_git/dart_git.dart' as git; import 'package:git_bindings/git_bindings.dart'; import 'package:gitjournal/core/note.dart'; import 'package:gitjournal/core/notes_folder.dart'; import 'package:gitjournal/core/notes_folder_fs.dart'; import 'package:gitjournal/error_reporting.dart'; import 'package:gitjournal/settings.dart'; import 'package:gitjournal/utils/logger.dart'; bool useDartGit = false; class NoteRepoResult { bool error; String? noteFilePath; NoteRepoResult({ required this.error, this.noteFilePath, }); } class GitNoteRepository { final String gitDirPath; final GitRepo _gitRepo; final Settings settings; GitNoteRepository({ required this.gitDirPath, required this.settings, }) : _gitRepo = GitRepo(folderPath: gitDirPath) { // git-bindings aren't properly implemented in these platforms if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { useDartGit = true; } } Future _add(String pathSpec) async { if (useDartGit) { var repo = await git.GitRepository.load(gitDirPath); await repo.add(pathSpec); } else { await _gitRepo.add(pathSpec); } } Future _rm(String pathSpec) async { if (useDartGit) { var repo = await git.GitRepository.load(gitDirPath); await repo.rm(pathSpec); } else { await _gitRepo.rm(pathSpec); } } Future _commit( {required String /*!*/ message, required String authorEmail, required String authorName}) async { if (useDartGit) { var repo = await git.GitRepository.load(gitDirPath); var author = git.GitAuthor(name: authorName, email: authorEmail); await repo.commit(message: message, author: author); } else { await _gitRepo.commit( message: message, authorEmail: settings.gitAuthorEmail, authorName: settings.gitAuthor, ); } } Future addNote(Note note) async { return _addNote(note, "Added Note"); } Future _addNote(Note note, String commitMessage) async { await _add("."); await _commit( message: commitMessage, authorEmail: settings.gitAuthorEmail, authorName: settings.gitAuthor, ); return NoteRepoResult(noteFilePath: note.filePath, error: false); } Future addFolder(NotesFolderFS folder) async { await _add("."); await _commit( message: "Created New Folder", authorEmail: settings.gitAuthorEmail, authorName: settings.gitAuthor, ); return NoteRepoResult(noteFilePath: folder.folderPath, error: false); } Future addFolderConfig(NotesFolderConfig config) async { var pathSpec = config.folder!.pathSpec(); pathSpec = pathSpec.isNotEmpty ? pathSpec : '/'; await _add("."); await _commit( message: "Update folder config for $pathSpec", authorEmail: settings.gitAuthorEmail, authorName: settings.gitAuthor, ); return NoteRepoResult( noteFilePath: config.folder!.folderPath, error: false); } Future renameFolder( String oldFullPath, String newFullPath, ) async { // FIXME: This is a hacky way of adding the changes, ideally we should be calling rm + add or something await _add("."); await _commit( message: "Renamed Folder", authorEmail: settings.gitAuthorEmail, authorName: settings.gitAuthor, ); return NoteRepoResult(noteFilePath: newFullPath, error: false); } Future renameNote( String oldFullPath, String newFullPath, ) async { // FIXME: This is a hacky way of adding the changes, ideally we should be calling rm + add or something await _add("."); await _commit( message: "Renamed Note", authorEmail: settings.gitAuthorEmail, authorName: settings.gitAuthor, ); return NoteRepoResult(noteFilePath: newFullPath, error: false); } Future renameFile( String oldFullPath, String newFullPath, ) async { // FIXME: This is a hacky way of adding the changes, ideally we should be calling rm + add or something await _add("."); await _commit( message: "Renamed File", authorEmail: settings.gitAuthorEmail, authorName: settings.gitAuthor, ); return NoteRepoResult(noteFilePath: newFullPath, error: false); } Future moveNote( String oldFullPath, String newFullPath, ) async { // FIXME: This is a hacky way of adding the changes, ideally we should be calling rm + add or something await _add("."); await _commit( message: "Note Moved", authorEmail: settings.gitAuthorEmail, authorName: settings.gitAuthor, ); return NoteRepoResult(noteFilePath: newFullPath, error: false); } Future removeNote(Note note) async { // We are not calling note.remove() as gitRm will also remove the file var spec = note.pathSpec(); await _rm(spec); await _commit( message: "Removed Note " + spec, authorEmail: settings.gitAuthorEmail, authorName: settings.gitAuthor, ); return NoteRepoResult(noteFilePath: note.filePath, error: false); } Future removeFolder(NotesFolderFS folder) async { var spec = folder.pathSpec(); await _rm(spec); await _commit( message: "Removed Folder " + spec, authorEmail: settings.gitAuthorEmail, authorName: settings.gitAuthor, ); return NoteRepoResult(noteFilePath: folder.folderPath, error: false); } Future resetLastCommit() async { await _gitRepo.resetLast(); return NoteRepoResult(error: false); } Future updateNote(Note note) async { return _addNote(note, "Edited Note"); } Future fetch() async { try { await _gitRepo.fetch( remote: "origin", publicKey: settings.sshPublicKey, privateKey: settings.sshPrivateKey, password: settings.sshPassword, ); } on GitException catch (ex, stackTrace) { Log.e("GitPull Failed", ex: ex, stacktrace: stackTrace); } } Future merge() async { var repo = await git.GitRepository.load(gitDirPath); var branch = await repo.currentBranch(); if (branch == null) { throw Exception('No current branch found'); } var branchConfig = repo.config.branch(branch); if (branchConfig == null) { logExceptionWarning( Exception("Branch '$branch' not in config"), StackTrace.current); return; } var remoteRef = await repo.remoteBranch( branchConfig.remote!, branchConfig.trackingBranch()!, ); if (remoteRef == null) { Log.i('Remote has no refs'); return; } try { await _gitRepo.merge( branch: branchConfig.remoteTrackingBranch(), authorEmail: settings.gitAuthorEmail, authorName: settings.gitAuthor, ); } on GitException catch (ex, stackTrace) { Log.e("Git Merge Failed", ex: ex, stacktrace: stackTrace); } } Future push() async { // Only push if we have something we need to push try { var repo = await git.GitRepository.load(gitDirPath); if ((await repo.canPush()) == false) { return; } } catch (_) {} try { await _gitRepo.push( remote: "origin", publicKey: settings.sshPublicKey, privateKey: settings.sshPrivateKey, password: settings.sshPassword, ); } on GitException catch (ex, stackTrace) { if (ex.cause == 'cannot push non-fastforwardable reference') { await fetch(); await merge(); return push(); } Log.e("GitPush Failed", ex: ex, stacktrace: stackTrace); rethrow; } } Future numChanges() async { try { var repo = await git.GitRepository.load(gitDirPath); var n = await repo.numChangesToPush(); return n; } catch (ex, st) { Log.e("numChanges", ex: ex, stacktrace: st); } return null; } } const ignoredMessages = [ 'connection timed out', 'failed to resolve address for', 'failed to connect to', 'no address associated with hostname', 'unauthorized', 'invalid credentials', 'failed to start ssh session', 'failure while draining', 'network is unreachable', 'software caused connection abort', 'unable to exchange encryption keys', 'the key you are authenticating with has been marked as read only', 'transport read', "unpacking the sent packfile failed on the remote", "key permission denied", // gogs "failed getting response", ]; bool shouldLogGitException(Exception ex) { if (ex is! GitException) { return false; } var msg = ex.cause.toLowerCase(); for (var i = 0; i < ignoredMessages.length; i++) { if (msg.contains(ignoredMessages[i])) { return false; } } return true; }