Improve repo migrations

Earlier we had one folder 'journal_local', when the remote would be
setup a new folder called 'journal' would be created, and each all the
files would be copied over. This meant the local history was being
destroyed.

Now, we only have 1 folder 'journal', and on 'cloning', we add the url
as a remote, and do a git fetch + merge.

This simplifies everything drastically, and opens the door for multiple
remotes.
This commit is contained in:
Vishesh Handa
2020-10-15 12:07:35 +02:00
parent 0839b8e38a
commit 540078c413
8 changed files with 93 additions and 139 deletions

View File

@ -1,49 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:git_bindings/git_bindings.dart';
import 'package:path/path.dart' as p;
import 'package:gitjournal/utils/logger.dart';
//
// FIXME: This isn't ideal as we are skipping all the edits / deletes
//
Future migrateGitRepo({
@required String gitBasePath,
@required String fromGitBasePath,
@required String toGitBaseFolder,
@required String gitAuthor,
@required String gitAuthorEmail,
}) async {
Log.d("migrateGitRepo $fromGitBasePath $toGitBaseFolder");
var fromBasePath = p.join(gitBasePath, fromGitBasePath);
var toGitRepoPath = p.join(gitBasePath, toGitBaseFolder);
Log.d("toGitRemotePath $toGitRepoPath");
final dir = Directory(fromBasePath);
var lister = dir.list(recursive: false);
await for (var fileEntity in lister) {
if (fileEntity is! File) {
continue;
}
File file = fileEntity;
var fileName = p.basename(file.path);
var toPath = p.join(toGitRepoPath, fileName);
Log.d("Migrating " + file.path + " --> " + toPath);
await file.copy(toPath);
var gitRepo = GitRepo(folderPath: toGitRepoPath);
await gitRepo.add(fileName);
await gitRepo.commit(
message: "Added Note",
authorEmail: gitAuthorEmail,
authorName: gitAuthor,
);
}
Log.d("migrateGitRepo: Done");
}

View File

@ -67,12 +67,16 @@ class JournalApp extends StatefulWidget {
} }
Log.i('-----'); Log.i('-----');
await settings.migrate(pref, appState.gitBaseDirectory);
// FIXME: This can be replaced with a fs stat
if (settings.localGitRepoConfigured == false) { if (settings.localGitRepoConfigured == false) {
settings.internalRepoFolderName = "journal";
// FIXME: What about exceptions! // FIXME: What about exceptions!
settings.localGitRepoFolderName = "journal_local";
var repoPath = p.join( var repoPath = p.join(
appState.gitBaseDirectory, appState.gitBaseDirectory,
settings.localGitRepoFolderName, settings.internalRepoFolderName,
); );
await GitRepository.init(repoPath); await GitRepository.init(repoPath);
@ -381,8 +385,9 @@ class _JournalAppState extends State<JournalApp> {
return SettingsScreen(); return SettingsScreen();
case '/setupRemoteGit': case '/setupRemoteGit':
return GitHostSetupScreen( return GitHostSetupScreen(
"journal", repoFolderName: settings.internalRepoFolderName,
stateContainer.completeGitHostSetup, remoteName: "origin",
onCompletedFunction: stateContainer.completeGitHostSetup,
); );
case '/onBoarding': case '/onBoarding':
return OnBoardingScreen(); return OnBoardingScreen();

View File

@ -180,14 +180,18 @@ class GitNoteRepository {
Future<void> merge() async { Future<void> merge() async {
var repo = await git.GitRepository.load(gitDirPath); var repo = await git.GitRepository.load(gitDirPath);
var branch = await repo.currentBranch(); var branch = await repo.currentBranch();
if (branch == null) { var branchConfig = repo.config.branch(branch);
if (branchConfig == null) {
logExceptionWarning(Exception("Current Branch null"), StackTrace.current); logExceptionWarning(Exception("Current Branch null"), StackTrace.current);
return; return;
} }
assert(branchConfig.name != null);
assert(branchConfig.merge != null);
try { try {
await _gitRepo.merge( await _gitRepo.merge(
branch: branch.remoteTrackingBranch(), branch: branchConfig.remoteTrackingBranch(),
authorEmail: settings.gitAuthorEmail, authorEmail: settings.gitAuthorEmail,
authorName: settings.gitAuthor, authorName: settings.gitAuthor,
); );

View File

@ -148,8 +148,9 @@ class _GitRemoteSettingsScreenState extends State<GitRemoteSettingsScreen> {
var route = MaterialPageRoute( var route = MaterialPageRoute(
builder: (context) => GitHostSetupScreen( builder: (context) => GitHostSetupScreen(
repoFolderName, repoFolderName: repoFolderName,
stateContainer.completeGitHostSetup, remoteName: 'origin',
onCompletedFunction: stateContainer.completeGitHostSetup,
), ),
settings: const RouteSettings(name: '/setupRemoteGit'), settings: const RouteSettings(name: '/setupRemoteGit'),
); );

View File

@ -1,12 +1,16 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:path/path.dart' as p;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:gitjournal/core/sorting_mode.dart'; import 'package:gitjournal/core/sorting_mode.dart';
import 'package:gitjournal/folder_views/common.dart'; import 'package:gitjournal/folder_views/common.dart';
import 'package:gitjournal/screens/note_editor.dart'; import 'package:gitjournal/screens/note_editor.dart';
import 'package:gitjournal/utils/logger.dart';
class Settings extends ChangeNotifier { class Settings extends ChangeNotifier {
Settings(); Settings();
@ -34,7 +38,7 @@ class Settings extends ChangeNotifier {
SettingsFolderViewType defaultView = SettingsFolderViewType.Default; SettingsFolderViewType defaultView = SettingsFolderViewType.Default;
bool showNoteSummary = true; bool showNoteSummary = true;
String folderViewHeaderType = "TitleGenerated"; String folderViewHeaderType = "TitleGenerated";
int version = 0; int version = 1;
SettingsHomeScreen homeScreen = SettingsHomeScreen.Default; SettingsHomeScreen homeScreen = SettingsHomeScreen.Default;
@ -56,10 +60,9 @@ class Settings extends ChangeNotifier {
bool bottomMenuBar = false; bool bottomMenuBar = false;
// From AppState // From AppState
String localGitRepoFolderName = ""; String internalRepoFolderName = "";
bool localGitRepoConfigured = false; bool localGitRepoConfigured = false;
String remoteGitRepoFolderName = "";
bool remoteGitRepoConfigured = false; bool remoteGitRepoConfigured = false;
bool storeInternally = true; bool storeInternally = true;
@ -131,8 +134,7 @@ class Settings extends ChangeNotifier {
// From AppState // From AppState
localGitRepoConfigured = pref.getBool("localGitRepoConfigured") ?? false; localGitRepoConfigured = pref.getBool("localGitRepoConfigured") ?? false;
remoteGitRepoConfigured = pref.getBool("remoteGitRepoConfigured") ?? false; remoteGitRepoConfigured = pref.getBool("remoteGitRepoConfigured") ?? false;
localGitRepoFolderName = pref.getString("localGitRepoPath") ?? ""; internalRepoFolderName = pref.getString("remoteGitRepoPath") ?? "";
remoteGitRepoFolderName = pref.getString("remoteGitRepoPath") ?? "";
bottomMenuBar = pref.getBool("bottomMenuBar") ?? bottomMenuBar; bottomMenuBar = pref.getBool("bottomMenuBar") ?? bottomMenuBar;
storeInternally = pref.getBool("storeInternally") ?? storeInternally; storeInternally = pref.getBool("storeInternally") ?? storeInternally;
@ -221,8 +223,7 @@ class Settings extends ChangeNotifier {
pref.setBool("localGitRepoConfigured", localGitRepoConfigured); pref.setBool("localGitRepoConfigured", localGitRepoConfigured);
pref.setBool("remoteGitRepoConfigured", remoteGitRepoConfigured); pref.setBool("remoteGitRepoConfigured", remoteGitRepoConfigured);
pref.setString("localGitRepoPath", localGitRepoFolderName); pref.setString("remoteGitRepoPath", internalRepoFolderName);
pref.setString("remoteGitRepoPath", remoteGitRepoFolderName);
notifyListeners(); notifyListeners();
} }
@ -302,8 +303,7 @@ class Settings extends ChangeNotifier {
'emojiParser': emojiParser.toString(), 'emojiParser': emojiParser.toString(),
'localGitRepoConfigured': localGitRepoConfigured.toString(), 'localGitRepoConfigured': localGitRepoConfigured.toString(),
'remoteGitRepoConfigured': remoteGitRepoConfigured.toString(), 'remoteGitRepoConfigured': remoteGitRepoConfigured.toString(),
'localGitRepoFolderName': localGitRepoFolderName.toString(), 'remoteGitRepoPath': internalRepoFolderName.toString(),
'remoteGitRepoFolderName': remoteGitRepoFolderName.toString(),
'bottomMenuBar': bottomMenuBar.toString(), 'bottomMenuBar': bottomMenuBar.toString(),
'storeInternally': storeInternally.toString(), 'storeInternally': storeInternally.toString(),
'storageLocation': storageLocation, 'storageLocation': storageLocation,
@ -317,6 +317,24 @@ class Settings extends ChangeNotifier {
m.remove("defaultNewNoteFolderSpec"); m.remove("defaultNewNoteFolderSpec");
return m; return m;
} }
Future<void> migrate(SharedPreferences pref, String gitBaseDir) async {
if (version == 0) {
if (localGitRepoConfigured && !remoteGitRepoConfigured) {
Log.i("Migrating from local and remote repos to a single one");
var oldName = p.join(gitBaseDir, "journal_local");
var newName = p.join(gitBaseDir, "journal");
await Directory(oldName).rename(newName);
internalRepoFolderName = "journal";
version = 1;
pref.setInt("settingsVersion", version);
pref.setString('remoteGitRepoPath', internalRepoFolderName);
return;
}
}
}
} }
class NoteFileNameFormat { class NoteFileNameFormat {

View File

@ -3,10 +3,11 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:dart_git/git.dart';
import 'package:dots_indicator/dots_indicator.dart'; import 'package:dots_indicator/dots_indicator.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:function_types/function_types.dart'; import 'package:function_types/function_types.dart';
import 'package:git_bindings/git_bindings.dart'; import 'package:git_bindings/git_bindings.dart' as git_bindings;
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -27,9 +28,14 @@ import 'package:gitjournal/utils/logger.dart';
class GitHostSetupScreen extends StatefulWidget { class GitHostSetupScreen extends StatefulWidget {
final String repoFolderName; final String repoFolderName;
final Func1<String, void> onCompletedFunction; final String remoteName;
final Func2<String, String, void> onCompletedFunction;
GitHostSetupScreen(this.repoFolderName, this.onCompletedFunction); GitHostSetupScreen({
@required this.repoFolderName,
@required this.remoteName,
@required this.onCompletedFunction,
});
@override @override
GitHostSetupScreenState createState() { GitHostSetupScreenState createState() {
@ -203,7 +209,10 @@ class GitHostSetupScreenState extends State<GitHostSetupScreen> {
} else if (_keyGenerationChoice == KeyGenerationChoice.UserProvided) { } else if (_keyGenerationChoice == KeyGenerationChoice.UserProvided) {
return GitHostUserProvidedKeys( return GitHostUserProvidedKeys(
doneFunction: (String publicKey, String privateKey) async { doneFunction: (String publicKey, String privateKey) async {
await setSshKeys(publicKey: publicKey, privateKey: privateKey); await git_bindings.setSshKeys(
publicKey: publicKey,
privateKey: privateKey,
);
setState(() { setState(() {
this.publicKey = publicKey; this.publicKey = publicKey;
_pageCount = pos + 2; _pageCount = pos + 2;
@ -284,7 +293,8 @@ class GitHostSetupScreenState extends State<GitHostSetupScreen> {
} else if (_keyGenerationChoice == KeyGenerationChoice.UserProvided) { } else if (_keyGenerationChoice == KeyGenerationChoice.UserProvided) {
return GitHostUserProvidedKeys( return GitHostUserProvidedKeys(
doneFunction: (String publicKey, String privateKey) async { doneFunction: (String publicKey, String privateKey) async {
await setSshKeys(publicKey: publicKey, privateKey: privateKey); await git_bindings.setSshKeys(
publicKey: publicKey, privateKey: privateKey);
setState(() { setState(() {
this.publicKey = publicKey; this.publicKey = publicKey;
_pageCount = pos + 2; _pageCount = pos + 2;
@ -399,7 +409,7 @@ class GitHostSetupScreenState extends State<GitHostSetupScreen> {
"-" + "-" +
DateTime.now().toIso8601String().substring(0, 10); // only the date DateTime.now().toIso8601String().substring(0, 10); // only the date
generateSSHKeys(comment: comment).then((String publicKey) { git_bindings.generateSSHKeys(comment: comment).then((String publicKey) {
setState(() { setState(() {
this.publicKey = publicKey; this.publicKey = publicKey;
Log.d("PublicKey: " + publicKey); Log.d("PublicKey: " + publicKey);
@ -471,17 +481,21 @@ class GitHostSetupScreenState extends State<GitHostSetupScreen> {
var stateContainer = Provider.of<StateContainer>(context, listen: false); var stateContainer = Provider.of<StateContainer>(context, listen: false);
var basePath = stateContainer.appState.gitBaseDirectory; var basePath = stateContainer.appState.gitBaseDirectory;
// Just in case it was half cloned because of an error var settings = Provider.of<Settings>(context, listen: false);
String repoPath = p.join(basePath, widget.repoFolderName); var repoName = settings.internalRepoFolderName;
await _removeExistingClone(repoPath); var repoPath = p.join(basePath, repoName);
Log.i("RepoPath: $repoPath");
String error; String error;
try { try {
Log.d("Cloning " + _gitCloneUrl); var repo = await GitRepository.load(repoPath);
await GitRepo.clone(repoPath, _gitCloneUrl); await repo.addRemote(widget.remoteName, _gitCloneUrl);
} on GitException catch (e) {
var repoN = git_bindings.GitRepo(folderPath: repoPath);
await repoN.fetch(widget.remoteName);
} on Exception catch (e) {
Log.e(e.toString()); Log.e(e.toString());
error = e.cause; error = e.toString();
} }
if (error != null && error.isNotEmpty) { if (error != null && error.isNotEmpty) {
@ -507,9 +521,7 @@ class GitHostSetupScreenState extends State<GitHostSetupScreen> {
var ignoreFile = File(p.join(repoPath, ".gitignore")); var ignoreFile = File(p.join(repoPath, ".gitignore"));
ignoreFile.createSync(); ignoreFile.createSync();
var repo = GitRepo( var repo = git_bindings.GitRepo(folderPath: repoPath);
folderPath: repoPath,
);
await repo.add('.gitignore'); await repo.add('.gitignore');
var settings = Provider.of<Settings>(context, listen: false); var settings = Provider.of<Settings>(context, listen: false);
@ -525,7 +537,7 @@ class GitHostSetupScreenState extends State<GitHostSetupScreen> {
parameters: _buildOnboardingAnalytics(), parameters: _buildOnboardingAnalytics(),
); );
Navigator.pop(context); Navigator.pop(context);
widget.onCompletedFunction(widget.repoFolderName); widget.onCompletedFunction(widget.repoFolderName, widget.remoteName);
} }
Future<void> _completeAutoConfigure() async { Future<void> _completeAutoConfigure() async {
@ -536,7 +548,7 @@ class GitHostSetupScreenState extends State<GitHostSetupScreen> {
setState(() { setState(() {
_autoConfigureMessage = tr('setup.sshKey.generate'); _autoConfigureMessage = tr('setup.sshKey.generate');
}); });
var publicKey = await generateSSHKeys(comment: "GitJournal"); var publicKey = await git_bindings.generateSSHKeys(comment: "GitJournal");
Log.i("Adding as a deploy key"); Log.i("Adding as a deploy key");
_autoConfigureMessage = tr('setup.sshKey.addDeploy'); _autoConfigureMessage = tr('setup.sshKey.addDeploy');
@ -594,17 +606,6 @@ class GitHostSetupScreenState extends State<GitHostSetupScreen> {
return map; return map;
} }
Future _removeExistingClone(String baseDirPath) async {
var baseDir = Directory(baseDirPath);
var dotGitDir = Directory(p.join(baseDir.path, ".git"));
bool exists = dotGitDir.existsSync();
if (exists) {
Log.d("Removing " + baseDir.path);
await baseDir.delete(recursive: true);
await baseDir.create();
}
}
} }
class GitHostChoicePage extends StatelessWidget { class GitHostChoicePage extends StatelessWidget {

View File

@ -4,11 +4,11 @@ import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dart_git/dart_git.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:synchronized/synchronized.dart'; import 'package:synchronized/synchronized.dart';
import 'package:gitjournal/analytics.dart'; import 'package:gitjournal/analytics.dart';
import 'package:gitjournal/apis/git_migration.dart';
import 'package:gitjournal/appstate.dart'; import 'package:gitjournal/appstate.dart';
import 'package:gitjournal/core/git_repo.dart'; import 'package:gitjournal/core/git_repo.dart';
import 'package:gitjournal/core/note.dart'; import 'package:gitjournal/core/note.dart';
@ -40,22 +40,11 @@ class StateContainer with ChangeNotifier {
@required this.gitBaseDirectory, @required this.gitBaseDirectory,
}) { }) {
assert(settings.localGitRepoConfigured); assert(settings.localGitRepoConfigured);
var repoPath = p.join(gitBaseDirectory, settings.internalRepoFolderName);
String repoPath;
if (settings.remoteGitRepoConfigured) {
repoPath = p.join(gitBaseDirectory, settings.remoteGitRepoFolderName);
} else if (settings.localGitRepoConfigured) {
repoPath = p.join(gitBaseDirectory, settings.localGitRepoFolderName);
}
_gitRepo = GitNoteRepository(gitDirPath: repoPath, settings: settings); _gitRepo = GitNoteRepository(gitDirPath: repoPath, settings: settings);
appState.notesFolder = NotesFolderFS(null, _gitRepo.gitDirPath, settings); appState.notesFolder = NotesFolderFS(null, _gitRepo.gitDirPath, settings);
// Just a fail safe
if (!settings.remoteGitRepoConfigured) {
removeExistingRemoteClone();
}
// Makes it easier to filter the analytics // Makes it easier to filter the analytics
getAnalytics().firebase.setUserProperty( getAnalytics().firebase.setUserProperty(
name: 'onboarded', name: 'onboarded',
@ -81,17 +70,6 @@ class StateContainer with ChangeNotifier {
Log.i("Finished loading all the notes"); Log.i("Finished loading all the notes");
} }
void removeExistingRemoteClone() async {
var remoteGitDir =
Directory(p.join(gitBaseDirectory, settings.remoteGitRepoFolderName));
var dotGitDir = Directory(p.join(remoteGitDir.path, ".git"));
bool exists = dotGitDir.existsSync();
if (exists) {
await remoteGitDir.delete(recursive: true);
}
}
Future<void> _loadNotes() async { Future<void> _loadNotes() async {
// FIXME: We should report the notes that failed to load // FIXME: We should report the notes that failed to load
return _loadLock.synchronized(() async { return _loadLock.synchronized(() async {
@ -351,29 +329,25 @@ class StateContainer with ChangeNotifier {
}); });
} }
void completeGitHostSetup(String repoFolderName) { // FIXME: Pass the remote name that was added
void completeGitHostSetup(String repoFolderName, String remoteName) {
() async { () async {
var reconfiguringRemote = settings.remoteGitRepoConfigured; var repo = await GitRepository.load(_gitRepo.gitDirPath);
var remote = repo.config.remote(remoteName);
var remoteBranchName = 'master';
// FIXME: How to get this?
//
// There is no way to get it, just need to iterate over the refs
// and look for one!
await repo.setUpstreamTo(remote, remoteBranchName);
// At this point the remote should have been added and fetched
await _gitRepo.merge();
settings.remoteGitRepoConfigured = true; settings.remoteGitRepoConfigured = true;
settings.remoteGitRepoFolderName = repoFolderName;
if (!reconfiguringRemote) {
await migrateGitRepo(
fromGitBasePath: settings.localGitRepoFolderName,
toGitBaseFolder: settings.remoteGitRepoFolderName,
gitBasePath: gitBaseDirectory,
gitAuthor: settings.gitAuthor,
gitAuthorEmail: settings.gitAuthorEmail,
);
}
var repoPath = p.join(gitBaseDirectory, settings.remoteGitRepoFolderName);
_gitRepo = GitNoteRepository(gitDirPath: repoPath, settings: settings);
appState.notesFolder.reset(_gitRepo.gitDirPath);
await _persistConfig(); await _persistConfig();
await _notesCache.clear();
_loadNotes(); _loadNotes();
_syncNotes(); _syncNotes();

View File

@ -181,7 +181,7 @@ packages:
description: description:
path: "." path: "."
ref: HEAD ref: HEAD
resolved-ref: "8e7e782f32f6cb1f39e39036d795f06ddecc56aa" resolved-ref: aed5883fd17219c778cdf2d0d613a873d0b20f6c
url: "https://github.com/GitJournal/dart_git.git" url: "https://github.com/GitJournal/dart_git.git"
source: git source: git
version: "0.0.1" version: "0.0.1"
@ -226,7 +226,7 @@ packages:
name: equatable name: equatable
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.1" version: "1.2.5"
ext_storage: ext_storage:
dependency: "direct main" dependency: "direct main"
description: description: