diff --git a/app/src/androidTest/java/com/orgzly/android/repos/DataRepositoryTest.java b/app/src/androidTest/java/com/orgzly/android/repos/DataRepositoryTest.java index 3db2b9425..407f98a71 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/DataRepositoryTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/DataRepositoryTest.java @@ -51,7 +51,7 @@ public void testRepoAndShelfSetup() throws IOException { testUtils.setupBook("local-book-1", ""); assertEquals("Local books", 1, dataRepository.getBooks().size()); - assertEquals("Remote books", 3, SyncUtils.getBooksFromAllRepos(dataRepository, null).size()); + assertEquals("Remote books", 3, SyncUtils.getBooksFromNonIntegrallySyncedRepos(dataRepository, null).size()); } @Test @@ -61,7 +61,7 @@ public void testLoadRook() throws IOException { testUtils.setupRook(repo, "mock://repo-a/remote-book-2.org", "", "1abcdef", 1300067156000L); testUtils.setupRook(repo, "mock://repo-a/remote-book-3.org", "", "2abcdef", 1200067156000L); - VersionedRook vrook = SyncUtils.getBooksFromAllRepos(dataRepository, null).get(0); + VersionedRook vrook = SyncUtils.getBooksFromNonIntegrallySyncedRepos(dataRepository, null).get(0); dataRepository.loadBookFromRepo(vrook); diff --git a/app/src/androidTest/java/com/orgzly/android/repos/LocalDbRepoTest.java b/app/src/androidTest/java/com/orgzly/android/repos/LocalDbRepoTest.java index c4d1ee6c8..3971a0475 100644 --- a/app/src/androidTest/java/com/orgzly/android/repos/LocalDbRepoTest.java +++ b/app/src/androidTest/java/com/orgzly/android/repos/LocalDbRepoTest.java @@ -32,7 +32,7 @@ public void testGetBooksFromAllRepos() throws IOException { Repo repo = testUtils.setupRepo(RepoType.MOCK, "mock://repo-a"); testUtils.setupRook(repo, "mock://repo-a/mock-book.org", "book content\n\n* First note\n** Second note", "rev1", 1234567890000L); - List books = SyncUtils.getBooksFromAllRepos(dataRepository, null); + List books = SyncUtils.getBooksFromNonIntegrallySyncedRepos(dataRepository, null); assertEquals(1, books.size()); @@ -78,7 +78,7 @@ public void testRetrievingBook() throws IOException { testUtils.setupRook(repo, "mock://repo-a/mock-book.org", "book content\n\n* First note\n** Second note", "rev1", 1234567890000L); SyncRepo syncRepo = testUtils.repoInstance(RepoType.MOCK, "mock://repo-a"); - VersionedRook vrook = SyncUtils.getBooksFromAllRepos(dataRepository, null).get(0); + VersionedRook vrook = SyncUtils.getBooksFromNonIntegrallySyncedRepos(dataRepository, null).get(0); File tmpFile = dataRepository.getTempBookFile(); try { diff --git a/app/src/main/java/com/orgzly/android/BookName.java b/app/src/main/java/com/orgzly/android/BookName.java index be99f2ed1..d71549d72 100644 --- a/app/src/main/java/com/orgzly/android/BookName.java +++ b/app/src/main/java/com/orgzly/android/BookName.java @@ -108,6 +108,10 @@ public static String lastPathSegment(String name, BookFormat format) { } } + public static String getNameFromUris(Uri repoUri, Uri fileUri) { + return fromRepoRelativePath(getRepoRelativePath(repoUri, fileUri)).getName(); + } + public static BookName fromRepoRelativePath(String repoRelativePath) { if (repoRelativePath != null) { Matcher m = PATTERN.matcher(repoRelativePath); diff --git a/app/src/main/java/com/orgzly/android/NotesOrgExporter.kt b/app/src/main/java/com/orgzly/android/NotesOrgExporter.kt index efe8eda2a..2f63ec68c 100644 --- a/app/src/main/java/com/orgzly/android/NotesOrgExporter.kt +++ b/app/src/main/java/com/orgzly/android/NotesOrgExporter.kt @@ -1,6 +1,5 @@ package com.orgzly.android -import android.util.Log import com.orgzly.R import com.orgzly.android.data.DataRepository import com.orgzly.android.data.mappers.OrgMapper diff --git a/app/src/main/java/com/orgzly/android/data/DataRepository.kt b/app/src/main/java/com/orgzly/android/data/DataRepository.kt index 3b93329b0..cd1fa2c49 100644 --- a/app/src/main/java/com/orgzly/android/data/DataRepository.kt +++ b/app/src/main/java/com/orgzly/android/data/DataRepository.kt @@ -224,10 +224,26 @@ class DataRepository @Inject constructor( } } + fun getBookViewsWithoutLink(): List { + return db.bookView().getWithoutLink() + } fun getBooksWithError(): List { return db.book().getWithActionType(BookAction.Type.ERROR) } + fun getModifiedBooksLinkedToRepo(repoId: Long): List { + val allModifiedBooks = db.book().getAllModified() + return allModifiedBooks.filter { book -> + getBookView(book.id)?.linkRepo?.id == repoId + } + } + + fun getBooksLinkedToRepo(repoId: Long): List { + return db.book().getAll().filter {book -> + getBookView(book.id)?.linkRepo?.id == repoId + } + } + fun getBookView(name: String): BookView? { return db.bookView().get(name) } @@ -448,6 +464,10 @@ class DataRepository @Inject constructor( db.bookSync().delete(bookId) } + fun setBookIsNotModified(bookId: Long) { + db.book().setIsNotModified(setOf(bookId)) + } + private fun updateBookIsModified(bookId: Long, isModified: Boolean, time: Long = System.currentTimeMillis()) { updateBookIsModified(setOf(bookId), isModified, time) } @@ -2337,15 +2357,42 @@ class DataRepository @Inject constructor( } } - fun getSyncRepos(): List { + fun getNonIntegrallySyncedRepos(): List { + val list = ArrayList() + for ((id, type, url) in getRepos()) { + if (type.isIntegrallySynced()) + continue + try { + list.add(getRepoInstance(id, type, url)) + } catch (e: Exception) { + e.printStackTrace() + } + } + return list + } + + fun getIntegrallySyncedRepos(): List { val list = ArrayList() for ((id, type, url) in getRepos()) { + if (!type.isIntegrallySynced()) + continue try { list.add(getRepoInstance(id, type, url)) } catch (e: Exception) { e.printStackTrace() } + } + return list + } + fun getAllSyncRepos(): List { + val list = ArrayList() + for ((id, type, url) in getRepos()) { + try { + list.add(getRepoInstance(id, type, url)) + } catch (e: Exception) { + e.printStackTrace() + } } return list } diff --git a/app/src/main/java/com/orgzly/android/db/dao/BookDao.kt b/app/src/main/java/com/orgzly/android/db/dao/BookDao.kt index 11a8147d9..d1d0aeddb 100644 --- a/app/src/main/java/com/orgzly/android/db/dao/BookDao.kt +++ b/app/src/main/java/com/orgzly/android/db/dao/BookDao.kt @@ -23,6 +23,12 @@ abstract class BookDao : BaseDao { @Query("SELECT * FROM books WHERE last_action_type = :type") abstract fun getWithActionType(type: BookAction.Type): List + @Query("SELECT * FROM books WHERE is_modified = 1") + abstract fun getAllModified(): List + + @Query("SELECT * FROM books") + abstract fun getAll(): List + @Insert abstract fun insertBooks(vararg books: Book): LongArray diff --git a/app/src/main/java/com/orgzly/android/db/dao/BookViewDao.kt b/app/src/main/java/com/orgzly/android/db/dao/BookViewDao.kt index 557a6d7af..315bb848e 100644 --- a/app/src/main/java/com/orgzly/android/db/dao/BookViewDao.kt +++ b/app/src/main/java/com/orgzly/android/db/dao/BookViewDao.kt @@ -14,6 +14,9 @@ abstract class BookViewDao { @Query("$QUERY WHERE books.name = :name GROUP BY books.id") abstract fun get(name: String): BookView? + @Query("$QUERY WHERE link_repo_id IS NULL GROUP BY books.id") + abstract fun getWithoutLink(): List + @Query("$QUERY GROUP BY books.id ORDER BY $ORDER_BY_NAME") abstract fun getAllFOrderByNameLiveData(): LiveData> diff --git a/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java b/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java index f626246a6..d9bd96ed7 100644 --- a/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java +++ b/app/src/main/java/com/orgzly/android/git/GitFileSynchronizer.java @@ -7,6 +7,8 @@ import android.net.Uri; import android.util.Log; +import androidx.annotation.Nullable; + import com.orgzly.BuildConfig; import com.orgzly.R; import com.orgzly.android.App; @@ -16,16 +18,25 @@ import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.ListBranchCommand; import org.eclipse.jgit.api.MergeResult; +import org.eclipse.jgit.api.RebaseCommand; +import org.eclipse.jgit.api.RebaseResult; import org.eclipse.jgit.api.ResetCommand; import org.eclipse.jgit.api.Status; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.PushResult; +import org.eclipse.jgit.transport.RefSpec; +import org.eclipse.jgit.transport.RemoteRefUpdate; +import org.eclipse.jgit.treewalk.AbstractTreeIterator; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.TreeWalk; import java.io.File; @@ -41,7 +52,7 @@ public class GitFileSynchronizer { private final static String TAG = GitFileSynchronizer.class.getName(); public final static String PRE_SYNC_MARKER_BRANCH = "orgzly-pre-sync-marker"; - + public final static String CONFLICT_BRANCH = "ORGZLY_CONFLICT"; private final Git git; private final GitPreferences preferences; private final Context context; @@ -63,11 +74,37 @@ public void retrieveLatestVersionOfFile( MiscUtils.copyFile(workTreeFile(repositoryPath), destination); } + public void hardResetToRemoteHead() throws GitAPIException { + git.reset().setMode(ResetCommand.ResetType.HARD).setRef("origin/" + preferences.branchName()).call(); + } + public InputStream openRepoFileInputStream(String repositoryPath) throws FileNotFoundException { return new FileInputStream(workTreeFile(repositoryPath)); } - private void fetch() throws IOException { + private AbstractTreeIterator prepareTreeParser(RevCommit commit) throws IOException { + // from the commit we can build the tree which allows us to construct the TreeParser + Repository repo = git.getRepository(); + try (RevWalk walk = new RevWalk(repo)) { + RevTree tree = walk.parseTree(commit.getTree().getId()); + CanonicalTreeParser treeParser = new CanonicalTreeParser(); + try (ObjectReader reader = repo.newObjectReader()) { + treeParser.reset(reader, tree.getId()); + } + walk.dispose(); + return treeParser; + } + } + + public List getCommitDiff(RevCommit oldCommit, RevCommit newCommit) throws GitAPIException, IOException { + return git.diff() + .setShowNameAndStatusOnly(true) + .setOldTree(prepareTreeParser(oldCommit)) + .setNewTree(prepareTreeParser(newCommit)) + .call(); + } + + public RevCommit fetch() throws IOException { try { if (BuildConfig.LOG_DEBUG) { LogUtils.d(TAG, String.format("Fetching Git repo from %s", preferences.remoteUri())); @@ -78,9 +115,11 @@ private void fetch() throws IOException { .setRemoveDeletedRefs(true)) .call(); } catch (GitAPIException e) { - e.printStackTrace(); + Log.e(TAG, e.toString()); throw new IOException(e.getMessage()); } + String currentBranch = git.getRepository().getBranch(); + return getCommit("origin/" + currentBranch); } public void checkoutSelected() throws GitAPIException { @@ -96,11 +135,29 @@ public boolean mergeWithRemote() throws IOException { git.getRepository().getBranch())); return doMerge(mergeTarget); } catch (GitAPIException e) { - e.printStackTrace(); + Log.e(TAG, e.toString()); } return false; } + public RevCommit getRemoteHead() throws IOException { + return getCommit("origin/" + git.getRepository().getBranch()); + } + + public RebaseResult rebase() throws IOException { + ensureRepoIsClean(); + RebaseResult result; + try { + result = git.rebase().setUpstream("origin/" + git.getRepository().getBranch()).call(); + if (!result.getStatus().isSuccessful()) { + git.rebase().setOperation(RebaseCommand.Operation.ABORT).call(); + } + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + return result; + } + private String getShortHash(ObjectId hash) { String shortHash = hash.getName(); try { @@ -111,7 +168,7 @@ private String getShortHash(ObjectId hash) { return shortHash; } - private String createMergeBranchName(String repositoryPath, ObjectId commitHash) { + private String createConflictBranchName(String repositoryPath, ObjectId commitHash) { String shortCommitHash = getShortHash(commitHash); repositoryPath = repositoryPath.replace(" ", "_"); String now = new SimpleDateFormat("yyyy-MM-dd_HHmmss").format(Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTime()); @@ -134,7 +191,7 @@ public boolean updateAndCommitFileFromRevisionAndMerge( if (BuildConfig.LOG_DEBUG) { LogUtils.d(TAG, String.format("originalBranch is set to %s", originalBranch)); } - String mergeBranch = createMergeBranchName(repoRelativePath, fileRevision); + String mergeBranch = createConflictBranchName(repoRelativePath, fileRevision); if (BuildConfig.LOG_DEBUG) { LogUtils.d(TAG, String.format("originalBranch is set to %s", originalBranch)); LogUtils.d(TAG, String.format("Temporary mergeBranch is set to %s", mergeBranch)); @@ -234,11 +291,71 @@ public void tryPushIfHeadDiffersFromRemote() { } catch (IOException ignored) {} if (localHead != null && !localHead.equals(remoteHead)) { - tryPush(); + push(); + } + } + + public void pushToConflictBranch() { + RefSpec refSpec = new RefSpec("HEAD:refs/heads/" + CONFLICT_BRANCH); + final var pushCommand = transportSetter().setTransport(git.push().setRefSpecs(refSpec).setForce(true)); + final Object monitor = new Object(); + App.EXECUTORS.diskIO().execute(() -> { + try { + Iterable results = (Iterable) pushCommand.call(); + if (!results.iterator().next().getMessages().isEmpty()) { + if (currentActivity != null) { + showSnackbar(currentActivity, results.iterator().next().getMessages()); + } + } + synchronized (monitor) { + monitor.notify(); + } + } catch (GitAPIException e) { + if (currentActivity != null) { + showSnackbar(currentActivity, String.format("Failed to push to conflict branch: %s", e.getMessage())); + } + } + }); + synchronized (monitor) { + try { + monitor.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } } } - public void tryPush() { + public RemoteRefUpdate pushWithResult() throws Exception { + final var pushCommand = transportSetter().setTransport( + git.push().setRemote(preferences.remoteName())); + final Object monitor = new Object(); + final RemoteRefUpdate[] result = new RemoteRefUpdate[1]; + final Exception[] exception = new Exception[1]; + + App.EXECUTORS.diskIO().execute(() -> { + try { + Iterable results = (Iterable) pushCommand.call(); + result[0] = results.iterator().next().getRemoteUpdates().iterator().next(); + synchronized (monitor) { + monitor.notify(); + } + } catch (Exception e) { + exception[0] = e; + } + }); + synchronized (monitor) { + try { + monitor.wait(); + } catch (InterruptedException e) { + Log.e(TAG, e.toString()); + } + } + if (exception[0] != null) + throw exception[0]; + return result[0]; + } + + public void push() { final var pushCommand = transportSetter().setTransport( git.push().setRemote(preferences.remoteName())); final Object monitor = new Object(); @@ -342,7 +459,7 @@ private void pushToRemoteIfEmpty() throws GitAPIException { .setListMode(ListBranchCommand.ListMode.REMOTE) .call(); if (remoteBranches.isEmpty()) { - tryPush(); + push(); } } @@ -423,6 +540,18 @@ private void updateAndCommitFile( } } + public void writeFileAndAddToIndex(File sourceFile, String repoRelativePath) throws IOException { + if (repoHasUnstagedChanges()) + throw new IOException("Working tree is in an unclean state; refusing to update."); + File destinationFile = workTreeFile(repoRelativePath); + MiscUtils.copyFile(sourceFile, destinationFile); + try { + git.add().addFilepattern(repoRelativePath).call(); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + } + private void commit(String message) throws GitAPIException { git.commit().setMessage(message).call(); } @@ -460,6 +589,37 @@ private boolean gitRepoIsClean() { } } + private boolean repoHasUnstagedChanges() { + Status status; + try { + status = git.status().call(); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + return (status.getModified().size() > 0 || + status.getUntracked().size() > 0 || + status.getUntrackedFolders().size() > 0); + } + + /** + * If any changes have been staged, commit them, otherwise do nothing. + * @throws IOException + */ + @Nullable + public RevCommit commitAnyStagedChanges() throws IOException { + if (!gitRepoIsClean()) { + if (repoHasUnstagedChanges()) + throw new IOException("Working tree is in an unclean state; refusing to update."); + try { + commit("Orgzly update"); + return currentHead(); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + } + return null; + } + private void ensureRepoIsClean() throws IOException { if (!gitRepoIsClean()) throw new IOException("Refusing to update because there are uncommitted changes."); diff --git a/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java b/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java index a4f575f28..7899a8a29 100644 --- a/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DatabaseRepo.java @@ -2,7 +2,11 @@ import android.net.Uri; +import androidx.annotation.Nullable; + +import com.orgzly.android.data.DataRepository; import com.orgzly.android.data.DbRepoBookRepository; +import com.orgzly.android.sync.SyncState; import com.orgzly.android.util.MiscUtils; import com.orgzly.android.util.UriUtils; @@ -78,6 +82,17 @@ public VersionedRook renameBook(Uri oldFullUri, String newName) { return dbRepo.renameBook(repoId, oldFullUri, toUri); } + @Nullable + @Override + public SyncState syncRepo(DataRepository dataRepository) throws IOException { + return null; + } + + @Override + public RepoType getType() { + return RepoType.MOCK; + } + @Override public void delete(Uri uri) throws IOException { dbRepo.deleteBook(uri); diff --git a/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java b/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java index aaaddca22..db0a5be47 100644 --- a/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DirectoryRepo.java @@ -4,9 +4,13 @@ import android.os.Build; import android.util.Log; +import androidx.annotation.Nullable; + import com.orgzly.android.BookName; import com.orgzly.android.LocalStorage; +import com.orgzly.android.data.DataRepository; import com.orgzly.android.db.entity.Repo; +import com.orgzly.android.sync.SyncState; import com.orgzly.android.util.MiscUtils; import com.orgzly.android.util.UriUtils; @@ -209,6 +213,17 @@ public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOExcepti return new VersionedRook(repoId, RepoType.DIRECTORY, repoUri, newUri, rev, mtime); } + @Nullable + @Override + public SyncState syncRepo(DataRepository dataRepository) throws IOException { + return null; + } + + @Override + public RepoType getType() { + return RepoType.DIRECTORY; + } + @Override public void delete(Uri uri) throws IOException { String path = uri.getPath(); diff --git a/app/src/main/java/com/orgzly/android/repos/DocumentRepo.java b/app/src/main/java/com/orgzly/android/repos/DocumentRepo.java index 03dc61eca..7d1952225 100644 --- a/app/src/main/java/com/orgzly/android/repos/DocumentRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DocumentRepo.java @@ -6,12 +6,15 @@ import android.provider.DocumentsContract; import android.util.Log; +import androidx.annotation.Nullable; import androidx.documentfile.provider.DocumentFile; import com.orgzly.BuildConfig; import com.orgzly.R; import com.orgzly.android.BookName; +import com.orgzly.android.data.DataRepository; import com.orgzly.android.db.entity.Repo; +import com.orgzly.android.sync.SyncState; import com.orgzly.android.util.LogUtils; import com.orgzly.android.util.MiscUtils; @@ -283,6 +286,17 @@ public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOExcepti return new VersionedRook(repoId, RepoType.DOCUMENT, repoUri, newUri, rev, mtime); } + @Nullable + @Override + public SyncState syncRepo(DataRepository dataRepository) throws IOException { + return null; + } + + @Override + public RepoType getType() { + return RepoType.DOCUMENT; + } + @Override public void delete(Uri uri) throws IOException { DocumentFile docFile = DocumentFile.fromSingleUri(context, uri); diff --git a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java index 21cfdf65c..2c51e15a3 100644 --- a/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/DropboxRepo.java @@ -4,8 +4,11 @@ import android.net.Uri; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.orgzly.android.BookName; +import com.orgzly.android.data.DataRepository; +import com.orgzly.android.sync.SyncState; import java.io.File; import java.io.IOException; @@ -68,6 +71,17 @@ public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOExcepti return client.move(repoUri, oldFullUri, newFullUri); } + @Nullable + @Override + public SyncState syncRepo(DataRepository dataRepository) throws IOException { + return null; + } + + @Override + public RepoType getType() { + return RepoType.DROPBOX; + } + @Override public void delete(Uri uri) throws IOException { client.delete(uri.getPath()); diff --git a/app/src/main/java/com/orgzly/android/repos/GitRepo.java b/app/src/main/java/com/orgzly/android/repos/GitRepo.java index 2cf1941e1..14e067c03 100644 --- a/app/src/main/java/com/orgzly/android/repos/GitRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/GitRepo.java @@ -1,26 +1,40 @@ package com.orgzly.android.repos; import static com.orgzly.android.git.GitFileSynchronizer.PRE_SYNC_MARKER_BRANCH; +import static com.orgzly.android.ui.AppSnackbarUtils.showSnackbar; import android.content.Context; import android.net.Uri; import android.util.Log; +import androidx.annotation.Nullable; + import com.orgzly.BuildConfig; +import com.orgzly.R; +import com.orgzly.android.App; +import com.orgzly.android.NotesOrgExporter; import com.orgzly.android.BookFormat; import com.orgzly.android.BookName; +import com.orgzly.android.data.DataRepository; +import com.orgzly.android.db.entity.Book; +import com.orgzly.android.db.entity.BookAction; +import com.orgzly.android.db.entity.BookView; import com.orgzly.android.db.entity.Repo; import com.orgzly.android.git.GitFileSynchronizer; import com.orgzly.android.git.GitPreferences; import com.orgzly.android.git.GitPreferencesFromRepoPrefs; import com.orgzly.android.git.GitTransportSetter; import com.orgzly.android.prefs.RepoPreferences; +import com.orgzly.android.sync.BookNamesake; +import com.orgzly.android.sync.BookSyncStatus; +import com.orgzly.android.sync.SyncState; import com.orgzly.android.util.LogUtils; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.ignore.IgnoreNode; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; @@ -29,6 +43,7 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.transport.RemoteRefUpdate; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.util.FileUtils; @@ -38,11 +53,15 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; public class GitRepo implements SyncRepo, TwoWaySyncRepo { private final static String TAG = GitRepo.class.getName(); private final long repoId; + private final RepoType type = RepoType.GIT; /** * Used as cause when we try to clone into a non-empty directory @@ -185,6 +204,14 @@ public boolean isAutoSyncSupported() { return true; } + /** + * N.B: NOT called during regular GitRepo syncing, only during force-loading. + * @param file The contents of this file should be stored at the remote location/repo + * @param repoRelativePath The contents ({@code file}) should be stored under this + * (non-encoded) name + * @return + * @throws IOException + */ public VersionedRook storeBook(File file, String repoRelativePath) throws IOException { File destination = synchronizer.workTreeFile(repoRelativePath); @@ -193,8 +220,8 @@ public VersionedRook storeBook(File file, String repoRelativePath) throws IOExce } else { synchronizer.addAndCommitNewFile(file, repoRelativePath); } - synchronizer.tryPush(); - return currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(repoRelativePath).build()); + synchronizer.push(); + return currentVersionedRook(Uri.parse(repoRelativePath)); } private RevWalk walk() { @@ -205,17 +232,24 @@ RevCommit getCommitFromRevisionString(String revisionString) throws IOException return walk().parseCommit(ObjectId.fromString(revisionString)); } + /** + * N.B: NOT called during regular GitRepo syncing, only during force-loading. + * @param repoRelativePath + * @param destination + * @return + * @throws IOException + */ @Override public VersionedRook retrieveBook(String repoRelativePath, File destination) throws IOException { - - Uri sourceUri = Uri.parse("/" + repoRelativePath); - - // Ensure our repo copy is up-to-date. This is necessary when force-loading a book. - synchronizer.mergeWithRemote(); - - synchronizer.retrieveLatestVersionOfFile(sourceUri.getPath(), destination); - - return currentVersionedRook(sourceUri); + synchronizer.fetch(); + try { + // Reset our entire working tree to the remote head + synchronizer.hardResetToRemoteHead(); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + synchronizer.retrieveLatestVersionOfFile(repoRelativePath, destination); + return currentVersionedRook(Uri.parse(repoRelativePath)); } @Override @@ -237,18 +271,6 @@ private VersionedRook currentVersionedRook(Uri uri) { return new VersionedRook(repoId, RepoType.GIT, getUri(), uri, commit.name(), mtime); } - public boolean isUnchanged() throws IOException { - // Check if the current head is unchanged. - // If so, we can read all the VersionedRooks from the database. - synchronizer.setBranchAndGetLatest(); - // If current HEAD is null, there are no commits, and this means there are no remote - // changes to handle. - if (synchronizer.currentHead() == null) return true; - if (synchronizer.currentHead().equals(synchronizer.getCommit(PRE_SYNC_MARKER_BRANCH))) - return true; - return false; - } - public List getBooks() throws IOException { List result = new ArrayList<>(); if (synchronizer.currentHead() == null) { @@ -284,7 +306,7 @@ public TreeFilter clone() { } }); while (walk.next()) { - result.add(currentVersionedRook(Uri.withAppendedPath(Uri.EMPTY, walk.getPathString()))); + result.add(currentVersionedRook(Uri.parse(walk.getPathString()))); } return result; } @@ -294,20 +316,277 @@ public Uri getUri() { } public void delete(Uri uri) throws IOException { - if (synchronizer.deleteFileFromRepo(uri)) synchronizer.tryPush(); + if (synchronizer.deleteFileFromRepo(uri)) synchronizer.push(); } public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOException { String oldPath = oldFullUri.toString().replaceFirst("^/", ""); String newPath = BookName.repoRelativePath(newName, BookFormat.ORG); if (synchronizer.renameFileInRepo(oldPath, newPath)) { - synchronizer.tryPush(); + synchronizer.push(); return currentVersionedRook(Uri.EMPTY.buildUpon().appendPath(newPath).build()); } else { return null; } } + @Nullable + @Override + public SyncState syncRepo(DataRepository dataRepository) throws Exception { + RevCommit remoteHeadBeforeFetch = synchronizer.getRemoteHead(); + RevCommit newRemoteHead = null; + List allLinkedBooks = dataRepository.getBooksLinkedToRepo(repoId); + // If there are no books at all linked to the repo, make sure we + // aren't missing anything (e.g. during first sync, right after cloning). + if (allLinkedBooks.isEmpty()) { + for (VersionedRook vrook : getBooks()) { + BookView bookView = loadBook(dataRepository, vrook); + storeBookStatus(dataRepository, bookView, BookSyncStatus.NO_BOOK_ONE_ROOK); + } + return null; + } + // Create map of all books which need syncing. Add all which are out of sync or never + // synced. + Map syncedBooks = new HashMap<>(); + for (Book book : allLinkedBooks) { + BookView bookView = dataRepository.getBookView(book.getId()); + assert bookView != null; + if (bookView.isOutOfSync() || !bookView.hasSync()) { + BookNamesake namesake = new BookNamesake(book.getName()); + namesake.setBook(dataRepository.getBookView(book.getId())); + syncedBooks.put(book.getName(), namesake); + } + } + // Link and add any orphan books, if possible + for (BookView bookView : dataRepository.getBookViewsWithoutLink()) { + if (dataRepository.getRepos().size() > 1) { + storeBookStatus(dataRepository, bookView, + BookSyncStatus.ONLY_BOOK_WITHOUT_LINK_AND_MULTIPLE_REPOS); + } else { + dataRepository.setLink(bookView.getBook().getId(), new Repo(repoId, RepoType.GIT, + getUri().toString())); + BookNamesake namesake = new BookNamesake(bookView.getBook().getName()); + namesake.setBook(bookView); + syncedBooks.put(bookView.getBook().getName(), namesake); + } + } + // Export all syncable books and add to index + for (BookNamesake namesake : syncedBooks.values()) { + dataRepository.setBookLastActionAndSyncStatus(namesake.getBook().getBook().getId(), + BookAction.forNow( + BookAction.Type.PROGRESS, + App.getAppContext().getString(R.string.syncing_in_progress))); + File tmpFile = dataRepository.getTempBookFile(); + try { + new NotesOrgExporter(dataRepository).exportBook(namesake.getBook().getBook(), tmpFile); + String repoRelativePath = BookName.getRepoRelativePath(namesake.getBook()); + synchronizer.writeFileAndAddToIndex(tmpFile, repoRelativePath); + } finally { + tmpFile.delete(); + } + namesake.setStatus(BookSyncStatus.BOOK_WITH_LINK_LOCAL_MODIFIED); + } + boolean rebaseWasAttempted = false; + if (!syncedBooks.isEmpty()) { + RevCommit newCommit = synchronizer.commitAnyStagedChanges(); + for (BookNamesake namesake : syncedBooks.values()) { + if (newCommit != null) { + VersionedRook vrook = new VersionedRook( + repoId, + RepoType.GIT, + getUri(), + Uri.parse(BookName.getRepoRelativePath(namesake.getBook())), + newCommit.name(), + (long)newCommit.getCommitTime()*1000 + ); + dataRepository.updateBookLinkAndSync(namesake.getBook().getBook().getId(), vrook); + } + dataRepository.setBookIsNotModified(namesake.getBook().getBook().getId()); + } + // Try pushing + RemoteRefUpdate pushResult = synchronizer.pushWithResult(); + if (pushResult == null) + throw new IOException("Git push failed unexpectedly"); + switch (pushResult.getStatus()) { + case OK: + case UP_TO_DATE: + for (BookNamesake namesake : syncedBooks.values()) { + storeBookStatus(dataRepository, namesake.getBook(), namesake.getStatus()); + } + break; + case REJECTED_NONFASTFORWARD: + case REJECTED_REMOTE_CHANGED: + // Try rebasing on latest remote head + newRemoteHead = synchronizer.fetch(); + switch (synchronizer.rebase().getStatus()) { + case FAST_FORWARD: // Only remote changes + case OK: // Remote and local changes + if (!syncedBooks.isEmpty()) { + synchronizer.push(); + for (BookNamesake namesake : syncedBooks.values()) { + storeBookStatus( + dataRepository, + namesake.getBook(), + namesake.getStatus() + ); + } + } + break; + default: + // Rebase failed; push to conflict branch + synchronizer.pushToConflictBranch(); + for (BookNamesake namesake : syncedBooks.values()) { + namesake.setStatus(BookSyncStatus.CONFLICT_SAVED_TO_TEMP_BRANCH); + storeBookStatus(dataRepository, namesake.getBook(), namesake.getStatus()); + } + } + rebaseWasAttempted = true; + break; + default: + throw new IOException("Error during git push: " + pushResult.getMessage()); + } + } else { + // No local changes, but fetch is needed to discover remote changes + newRemoteHead = synchronizer.fetch(); + } + if (newRemoteHead != null && !newRemoteHead.name().equals(remoteHeadBeforeFetch.name())) { + // There are remote changes. + // Ensure we have rebased on the remote head. + if (!rebaseWasAttempted) { + if (!synchronizer.rebase().getStatus().isSuccessful()) + throw new IOException("Unexpectedly failed to merge with Git remote branch"); + } + // Reload any changed books and update their statuses. + List remoteChanges; + remoteChanges = synchronizer.getCommitDiff(remoteHeadBeforeFetch, newRemoteHead); + for (DiffEntry changedFile : remoteChanges) { + BookSyncStatus status = null; + BookView bookView = null; + switch (changedFile.getChangeType()) { + case MODIFY: { + BookNamesake alreadyChanged = + syncedBooks.get(BookName.fromRepoRelativePath(changedFile.getNewPath()).getName()); + if (alreadyChanged != null) { + if (alreadyChanged.getStatus() == BookSyncStatus.CONFLICT_SAVED_TO_TEMP_BRANCH) + break; + // There were both local and remote changes, but no conflict + status = BookSyncStatus.CONFLICT_BOTH_BOOK_AND_ROOK_MODIFIED; + bookView = loadBook(dataRepository, changedFile.getNewPath()); + } else { + // There were only remote changes. Add the book to list of synced books. + status = BookSyncStatus.BOOK_WITH_LINK_AND_ROOK_MODIFIED; + bookView = loadBook(dataRepository, changedFile.getNewPath()); + BookNamesake namesake = new BookNamesake(bookView.getBook().getName()); + namesake.setBook(bookView); + namesake.setStatus(status); + syncedBooks.put(namesake.getName(), namesake); + } + break; + } + case ADD: { + if (BookName.isSupportedFormatFileName(changedFile.getNewPath())) { + bookView = loadBook(dataRepository, changedFile.getNewPath()); + status = BookSyncStatus.NO_BOOK_ONE_ROOK; + } + break; + } + case DELETE: { + String repoRelativePath = changedFile.getOldPath(); + bookView = dataRepository.getBookView(BookName.fromRepoRelativePath(repoRelativePath).getName()); // TODO: Test this + assert bookView != null; + // Just remove the book link; don't delete the book + dataRepository.setLink(bookView.getBook().getId(), null); + break; + } + // TODO: Handle RENAME, COPY + default: + throw new IOException("Unsupported remote change in Git repo (file renamed or copied)"); + } + if (status != null && bookView != null) { + storeBookStatus(dataRepository, bookView, status); + } + } + } + // Update the status of all untouched books. + for (Book book : allLinkedBooks) { + if (!syncedBooks.containsKey(book.getName())) { + storeBookStatus(dataRepository, dataRepository.getBookView(book.getId()), + BookSyncStatus.NO_CHANGE); + } + } + return null; + } + + @Override + public RepoType getType() { + return RepoType.GIT; + } + + private BookView loadBook(DataRepository dataRepository, String repoRelativePath) throws IOException { + BookView bookView; + File tmpFile = dataRepository.getTempBookFile(); + try { + synchronizer.retrieveLatestVersionOfFile(repoRelativePath, tmpFile); + VersionedRook vrook = currentVersionedRook(Uri.parse(repoRelativePath)); + BookName bookName = BookName.fromRook(vrook); + bookView = dataRepository.loadBookFromFile( + bookName.getName(), + bookName.getFormat(), + tmpFile, + vrook + ); + } finally { + tmpFile.delete(); + } + return bookView; + } + + private BookView loadBook(DataRepository dataRepository, VersionedRook vrook) throws IOException { + BookView bookView; + File tmpFile = dataRepository.getTempBookFile(); + try { + synchronizer.retrieveLatestVersionOfFile(BookName.fromRook(vrook).getRepoRelativePath(), tmpFile); + BookName bookName = BookName.fromRook(vrook); + bookView = dataRepository.loadBookFromFile( + bookName.getName(), + bookName.getFormat(), + tmpFile, + vrook + ); + } finally { + tmpFile.delete(); + } + return bookView; + } + + private String currentBranch() throws IOException { + return git.getRepository().getBranch(); + } + + private void storeBookStatus(DataRepository dataRepository, + BookView bookView, + BookSyncStatus status) throws IOException { + BookAction.Type actionType = BookAction.Type.INFO; + String actionMessageArgument = ""; + switch (status) { + case ONLY_BOOK_WITHOUT_LINK_AND_ONE_REPO: + case NO_BOOK_ONE_ROOK: + case BOOK_WITH_LINK_LOCAL_MODIFIED: + case BOOK_WITH_LINK_AND_ROOK_MODIFIED: + case ONLY_BOOK_WITH_LINK: + actionMessageArgument = String.format("branch \"%s\"", currentBranch()); + break; + case ONLY_BOOK_WITHOUT_LINK_AND_MULTIPLE_REPOS: + case CONFLICT_SAVED_TO_TEMP_BRANCH: + actionType = BookAction.Type.ERROR; + break; + } + BookAction action = BookAction.forNow(actionType, status.msg(actionMessageArgument)); + dataRepository.setBookLastActionAndSyncStatus(bookView.getBook().getId(), + action, + status.toString()); + } + @Override public TwoWaySyncResult syncBook( Uri uri, VersionedRook current, File fromDB) throws IOException { diff --git a/app/src/main/java/com/orgzly/android/repos/MockRepo.java b/app/src/main/java/com/orgzly/android/repos/MockRepo.java index df2bedbb0..95b537959 100644 --- a/app/src/main/java/com/orgzly/android/repos/MockRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/MockRepo.java @@ -4,14 +4,16 @@ import android.os.SystemClock; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; +import com.orgzly.android.data.DataRepository; import com.orgzly.android.data.DbRepoBookRepository; import com.orgzly.android.prefs.AppPreferences; +import com.orgzly.android.sync.SyncState; import java.io.ByteArrayInputStream; import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -90,6 +92,17 @@ public VersionedRook renameBook(Uri oldFullUri, String newName) throws IOExcepti return databaseRepo.renameBook(oldFullUri, newName); } + @Nullable + @Override + public SyncState syncRepo(DataRepository dataRepository) throws IOException { + return null; + } + + @Override + public RepoType getType() { + return RepoType.MOCK; + } + @Override public void delete(Uri uri) throws IOException { SystemClock.sleep(SLEEP_FOR_DELETE_BOOK); diff --git a/app/src/main/java/com/orgzly/android/repos/RepoType.kt b/app/src/main/java/com/orgzly/android/repos/RepoType.kt index e72cd0d49..f3394b433 100644 --- a/app/src/main/java/com/orgzly/android/repos/RepoType.kt +++ b/app/src/main/java/com/orgzly/android/repos/RepoType.kt @@ -1,7 +1,5 @@ package com.orgzly.android.repos -import java.lang.IllegalArgumentException - enum class RepoType(val id: Int) { MOCK(1), DROPBOX(2), @@ -10,6 +8,12 @@ enum class RepoType(val id: Int) { WEBDAV(5), GIT(6); + fun isIntegrallySynced(): Boolean { + return this in listOf( + GIT + ) + } + companion object { @JvmStatic fun fromId(type: Int): RepoType { diff --git a/app/src/main/java/com/orgzly/android/repos/SyncRepo.java b/app/src/main/java/com/orgzly/android/repos/SyncRepo.java index 11d6cfb33..32365610e 100644 --- a/app/src/main/java/com/orgzly/android/repos/SyncRepo.java +++ b/app/src/main/java/com/orgzly/android/repos/SyncRepo.java @@ -2,6 +2,11 @@ import android.net.Uri; +import androidx.annotation.Nullable; + +import com.orgzly.android.data.DataRepository; +import com.orgzly.android.sync.SyncState; + import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -58,6 +63,17 @@ public interface SyncRepo { */ VersionedRook renameBook(Uri oldFullUri, String newName) throws IOException; + /** + * Syncs all books linked to the repo, ignoring all other local books. If a repo + * rook without a namesake is found, an attempt must be made to create and link a namesake. + * @param dataRepository To know which books are modified and linked to this repo + * @return A SyncState if any problems were encountered + */ + @Nullable + SyncState syncRepo(DataRepository dataRepository) throws Exception; + + RepoType getType(); + // VersionedRook moveBook(Uri from, Uri uri) throws IOException; void delete(Uri uri) throws IOException; diff --git a/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt b/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt index 81599b893..f48d54607 100644 --- a/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt +++ b/app/src/main/java/com/orgzly/android/repos/WebdavRepo.kt @@ -10,6 +10,8 @@ import com.burgstaller.okhttp.digest.CachingAuthenticator import com.burgstaller.okhttp.digest.Credentials import com.burgstaller.okhttp.digest.DigestAuthenticator import com.orgzly.android.BookName +import com.orgzly.android.data.DataRepository +import com.orgzly.android.sync.SyncState import com.thegrizzlylabs.sardineandroid.DavResource import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine import okhttp3.OkHttpClient @@ -251,6 +253,14 @@ class WebdavRepo( return sardine.list(newFullUrl).first().toVersionedRook() } + override fun syncRepo(dataRepository: DataRepository?): SyncState? { + TODO("Not yet implemented") + } + + override fun getType(): RepoType { + return RepoType.WEBDAV + } + override fun delete(uri: Uri) { sardine.delete(uri.toUrl()) } diff --git a/app/src/main/java/com/orgzly/android/sync/BookNamesake.java b/app/src/main/java/com/orgzly/android/sync/BookNamesake.java index 7d86055f6..57182adb0 100644 --- a/app/src/main/java/com/orgzly/android/sync/BookNamesake.java +++ b/app/src/main/java/com/orgzly/android/sync/BookNamesake.java @@ -104,6 +104,9 @@ public String toString() { " | Remotes:" + versionedRooks.size() + "]"; } + public void setStatus(BookSyncStatus newStatus) { + status = newStatus; + } /** * States to consider: diff --git a/app/src/main/java/com/orgzly/android/sync/BookSyncStatus.kt b/app/src/main/java/com/orgzly/android/sync/BookSyncStatus.kt index ed76ccae9..17dd8427b 100644 --- a/app/src/main/java/com/orgzly/android/sync/BookSyncStatus.kt +++ b/app/src/main/java/com/orgzly/android/sync/BookSyncStatus.kt @@ -2,6 +2,7 @@ package com.orgzly.android.sync import com.orgzly.R import com.orgzly.android.App +import com.orgzly.android.git.GitFileSynchronizer // TODO: Write tests for *all* cases. enum class BookSyncStatus { @@ -20,6 +21,7 @@ enum class BookSyncStatus { CONFLICT_BOTH_BOOK_AND_ROOK_MODIFIED, CONFLICT_BOOK_WITH_LINK_AND_ROOK_BUT_NEVER_SYNCED_BEFORE, CONFLICT_LAST_SYNCED_ROOK_AND_LATEST_ROOK_ARE_DIFFERENT, + CONFLICT_SAVED_TO_TEMP_BRANCH, /* Book can be loaded. */ NO_BOOK_ONE_ROOK, // TODO: Can this happen? We always load dummy. @@ -76,6 +78,9 @@ enum class BookSyncStatus { CONFLICT_LAST_SYNCED_ROOK_AND_LATEST_ROOK_ARE_DIFFERENT -> return "Last synced notebook and latest remote notebook differ" + CONFLICT_SAVED_TO_TEMP_BRANCH -> + return "Sync conflict. Saved to branch \"${GitFileSynchronizer.CONFLICT_BRANCH}\"." + NO_BOOK_ONE_ROOK, DUMMY_WITHOUT_LINK_AND_ONE_ROOK, BOOK_WITH_LINK_AND_ROOK_MODIFIED, DUMMY_WITH_LINK -> return context.getString(R.string.sync_status_loaded, "$arg") diff --git a/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt b/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt index ddf69afe1..a1419dba9 100644 --- a/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt +++ b/app/src/main/java/com/orgzly/android/sync/SyncUtils.kt @@ -23,22 +23,14 @@ object SyncUtils { */ @Throws(IOException::class) @JvmStatic - fun getBooksFromAllRepos(dataRepository: DataRepository, repos: List? = null): List { + fun getBooksFromNonIntegrallySyncedRepos(dataRepository: DataRepository, repos: List? = null): List { val result = ArrayList() - val repoList = repos ?: dataRepository.getSyncRepos() + val repoList = repos ?: dataRepository.getAllSyncRepos() for (repo in repoList) { - if (repo is GitRepo && repo.isUnchanged) { - for (book in dataRepository.getBooks()) { - if (book.hasLink() && book.linkRepo!!.url == repo.uri.toString() && book.hasSync()) { - result.add(book.syncedTo!!) - } - } - if (result.isNotEmpty()) { - continue - } - } + if (repo.type.isIntegrallySynced()) + continue // Skip integrally synced repos val libBooks = repo.books /* Each book in repository. */ result.addAll(libBooks) @@ -57,13 +49,16 @@ object SyncUtils { fun groupAllNotebooksByName(dataRepository: DataRepository): Map { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Collecting all local and remote books ...") - val repos = dataRepository.getSyncRepos() + val repos = dataRepository.getAllSyncRepos() + + val syncableLocalBooks = dataRepository.getBooks().filterNot { + it.hasLink() && it.linkRepo?.type?.isIntegrallySynced() == true + } - val localBooks = dataRepository.getBooks() - val versionedRooks = getBooksFromAllRepos(dataRepository, repos) + val versionedRooks = getBooksFromNonIntegrallySyncedRepos(dataRepository, repos) /* Group local and remote books by name. */ - val namesakes = BookNamesake.getAll(localBooks, versionedRooks) + val namesakes = BookNamesake.getAll(syncableLocalBooks, versionedRooks) /* If there is no local book, create empty "dummy" one. */ for (namesake in namesakes.values) { @@ -121,6 +116,7 @@ object SyncUtils { BookSyncStatus.CONFLICT_BOTH_BOOK_AND_ROOK_MODIFIED, BookSyncStatus.CONFLICT_BOOK_WITH_LINK_AND_ROOK_BUT_NEVER_SYNCED_BEFORE, BookSyncStatus.CONFLICT_LAST_SYNCED_ROOK_AND_LATEST_ROOK_ARE_DIFFERENT, + BookSyncStatus.CONFLICT_SAVED_TO_TEMP_BRANCH, BookSyncStatus.ROOK_AND_VROOK_HAVE_DIFFERENT_REPOS, BookSyncStatus.ONLY_DUMMY, BookSyncStatus.BOOK_WITH_PREVIOUS_ERROR_AND_NO_LINK -> diff --git a/app/src/main/java/com/orgzly/android/sync/SyncWorker.kt b/app/src/main/java/com/orgzly/android/sync/SyncWorker.kt index 02f2b9bc9..ec2f8c000 100644 --- a/app/src/main/java/com/orgzly/android/sync/SyncWorker.kt +++ b/app/src/main/java/com/orgzly/android/sync/SyncWorker.kt @@ -17,7 +17,9 @@ import com.orgzly.android.data.logs.AppLogsRepository import com.orgzly.android.db.entity.BookAction import com.orgzly.android.prefs.AppPreferences import com.orgzly.android.reminders.RemindersScheduler -import com.orgzly.android.repos.* +import com.orgzly.android.repos.DirectoryRepo +import com.orgzly.android.repos.RepoUtils +import com.orgzly.android.repos.SyncRepo import com.orgzly.android.ui.notifications.SyncNotifications import com.orgzly.android.ui.util.getAlarmManager import com.orgzly.android.ui.util.haveNetworkConnection @@ -96,7 +98,9 @@ class SyncWorker(val context: Context, val params: WorkerParameters) : val syncStartTime = System.currentTimeMillis() - syncRepos()?.let { return it } + syncIntegrallySyncedRepos()?.let { return it } + + syncNamesakes()?.let { return it } RemindersScheduler.notifyDataSetChanged(App.getAppContext()) ListWidgetProvider.notifyDataSetChanged(App.getAppContext()) @@ -139,7 +143,7 @@ class SyncWorker(val context: Context, val params: WorkerParameters) : val autoSync = params.inputData.getBoolean(SyncRunner.IS_AUTO_SYNC, false) - val repos = dataRepository.getSyncRepos() + val repos = dataRepository.getAllSyncRepos() /* Do nothing if it's auto-sync and there are no repos or they require connection. */ if (autoSync) { @@ -195,9 +199,22 @@ class SyncWorker(val context: Context, val params: WorkerParameters) : return null } - private suspend fun syncRepos(): SyncState? { + private fun syncIntegrallySyncedRepos(): SyncState? { + val repos = dataRepository.getIntegrallySyncedRepos() + if (repos.isEmpty()) + return null + for (repo: SyncRepo in repos) { + repo.syncRepo(dataRepository)?.let { return it } + } + return null + } + + private suspend fun syncNamesakes(): SyncState? { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG) + if (dataRepository.getNonIntegrallySyncedRepos().isEmpty()) + return null + sendProgress(SyncState.getInstance(SyncState.Type.COLLECTING_BOOKS)) /* Get the list of local and remote books from all repositories. @@ -218,38 +235,10 @@ class SyncWorker(val context: Context, val params: WorkerParameters) : sendProgress(SyncState.getInstance(SyncState.Type.BOOKS_COLLECTED, total = namesakes.size)) - /* Because android sometimes drops milliseconds on reported file lastModified, - * wait until the next full second - */ - // if (isTriggeredAutomatically) { - // long now = System.currentTimeMillis(); - // long nowMsPart = now % 1000; - // SystemClock.sleep(1000 - nowMsPart); - // } - - /* If there are namesakes in Git repos with conflict status, make - * sure to sync them first, so that any conflict branches are - * created as early as possible. Otherwise, we risk committing - * changes on master which we cannot see on the conflict branch. - */ - val orderedNamesakes = LinkedHashMap() - val lowPriorityNamesakes = LinkedHashMap() - for (namesake in namesakes.values) { - if (namesake.rooks.isNotEmpty() && - namesake.rooks[0].repoType == RepoType.GIT && - namesake.status == BookSyncStatus.CONFLICT_BOTH_BOOK_AND_ROOK_MODIFIED - ) { - orderedNamesakes[namesake.name] = namesake - } else { - lowPriorityNamesakes[namesake.name] = namesake - } - } - orderedNamesakes.putAll(lowPriorityNamesakes) - /* * Update books' statuses, before starting to sync them. */ - for (namesake in orderedNamesakes.values) { + for (namesake in namesakes.values) { dataRepository.setBookLastActionAndSyncStatus(namesake.book.book.id, BookAction.forNow( BookAction.Type.PROGRESS, context.getString(R.string.syncing_in_progress))) } @@ -257,7 +246,7 @@ class SyncWorker(val context: Context, val params: WorkerParameters) : /* * Start syncing name by name. */ - for ((curr, namesake) in orderedNamesakes.values.withIndex()) { + for ((curr, namesake) in namesakes.values.withIndex()) { /* If task has been canceled, just mark the remaining books as such. */ if (isStopped) { dataRepository.setBookLastActionAndSyncStatus( @@ -290,14 +279,6 @@ class SyncWorker(val context: Context, val params: WorkerParameters) : return SyncState.getInstance(SyncState.Type.CANCELED) } - val repos = dataRepository.getSyncRepos() - - for (repo in repos) { - if (repo is TwoWaySyncRepo) { - repo.tryPushIfHeadDiffersFromRemote() - } - } - return null }