From cff34273287472422a6f99ff9cf3222a18b54191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CKhaoula?= <“khaoula.arouissi@imt-atlantique.net”> Date: Wed, 6 Nov 2024 09:44:46 +0100 Subject: [PATCH] first git implementation, not tested --- build.gradle | 1 + .../autosaveandbackup/BackUpManagerJGit.java | 162 ++++++++---------- .../gui/autosaveandbackup/BackupManager.java | 19 +- 3 files changed, 82 insertions(+), 100 deletions(-) diff --git a/build.gradle b/build.gradle index a324fc7f1575..6d46a47da511 100644 --- a/build.gradle +++ b/build.gradle @@ -351,6 +351,7 @@ dependencies { exclude group: 'org.jetbrains.kotlin' } + implementation 'org.eclipse.jgit:org.eclipse.jgit:5.13.0.202109080827-r' implementation platform('ai.djl:bom:0.30.0') implementation 'ai.djl:api' diff --git a/src/main/java/org/jabref/gui/autosaveandbackup/BackUpManagerJGit.java b/src/main/java/org/jabref/gui/autosaveandbackup/BackUpManagerJGit.java index 90d99ef2894d..bedeb80fc59b 100644 --- a/src/main/java/org/jabref/gui/autosaveandbackup/BackUpManagerJGit.java +++ b/src/main/java/org/jabref/gui/autosaveandbackup/BackUpManagerJGit.java @@ -1,15 +1,18 @@ package org.jabref.gui.autosaveandbackup; +import java.io.File; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.FileTime; import java.util.HashSet; -import java.util.Optional; import java.util.Set; import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.jabref.gui.LibraryTab; import org.jabref.logic.preferences.CliPreferences; import org.jabref.logic.util.CoarseChangeFilter; @@ -23,7 +26,6 @@ public class BackUpManagerJGit { private static final Logger LOGGER = LoggerFactory.getLogger(BackupManager.class); - private static final int DELAY_BETWEEN_BACKUP_ATTEMPTS_IN_SECONDS = 19; private static Set runningInstances = new HashSet<>(); @@ -34,13 +36,11 @@ public class BackUpManagerJGit { private final CoarseChangeFilter changeFilter; private final BibEntryTypesManager entryTypesManager; private final LibraryTab libraryTab; + private final Git git; - // Contains a list of all backup paths - // During writing, the less recent backup file is deleted - //private final Queue backupFilesQueue = new LinkedBlockingQueue<>(); private boolean needsBackup = false; - public BackUpManagerJGit(LibraryTab libraryTab, BibDatabaseContext bibDatabaseContext, BibEntryTypesManager entryTypesManager, CliPreferences preferences) { + BackupManager(LibraryTab libraryTab, BibDatabaseContext bibDatabaseContext, BibEntryTypesManager entryTypesManager, CliPreferences preferences) throws IOException, GitAPIException { this.bibDatabaseContext = bibDatabaseContext; this.entryTypesManager = entryTypesManager; this.preferences = preferences; @@ -49,93 +49,83 @@ public BackUpManagerJGit(LibraryTab libraryTab, BibDatabaseContext bibDatabaseCo changeFilter = new CoarseChangeFilter(bibDatabaseContext); changeFilter.registerListener(this); + + // Initialize Git repository + FileRepositoryBuilder builder = new FileRepositoryBuilder(); + git = new Git(builder.setGitDir(new File(preferences.getFilePreferences().getBackupDirectory().toFile(), ".git")) + .readEnvironment() + .findGitDir() + .build()); + if (git.getRepository().getObjectDatabase().exists()) { + LOGGER.info("Git repository already exists"); + } else { + git.init().call(); + LOGGER.info("Initialized new Git repository"); + } } - /** - * Starts the BackupManager which is associated with the given {@link BibDatabaseContext}. As long as no database - * file is present in {@link BibDatabaseContext}, the {@link BackupManager} will do nothing. - * - * This method is not thread-safe. The caller has to ensure that this method is not called in parallel. - * - * @param bibDatabaseContext Associated {@link BibDatabaseContext} - */ - - public static BackUpManagerJGit start(LibraryTab libraryTab, BibDatabaseContext bibDatabaseContext, BibEntryTypesManager entryTypesManager, CliPreferences preferences) { - BackUpManagerJGit backupManagerJGit = new BackUpManagerJGit(libraryTab, bibDatabaseContext, entryTypesManager, preferences); - backupManagerJGit.startBackupTask(preferences.getFilePreferences().getBackupDirectory()); - runningInstances.add(backupManagerJGit); - return backupManagerJGit; + + public static BackupManager start(LibraryTab libraryTab, BibDatabaseContext bibDatabaseContext, BibEntryTypesManager entryTypesManager, CliPreferences preferences) throws IOException, GitAPIException { + BackupManager backupManager = new BackupManager(libraryTab, bibDatabaseContext, entryTypesManager, preferences); + backupManager.startBackupTask(preferences.getFilePreferences().getBackupDirectory()); + runningInstances.add(backupManager); + return backupManager; } - /** - * Shuts down the BackupManager which is associated with the given {@link BibDatabaseContext}. - * - * @param bibDatabaseContext Associated {@link BibDatabaseContext} - * @param createBackup True, if a backup should be created - * @param backupDir The path to the backup directory - */ + public static void shutdown(BibDatabaseContext bibDatabaseContext, Path backupDir, boolean createBackup) { runningInstances.stream().filter(instance -> instance.bibDatabaseContext == bibDatabaseContext).forEach(backupManager -> backupManager.shutdown(backupDir, createBackup)); runningInstances.removeIf(instance -> instance.bibDatabaseContext == bibDatabaseContext); } - /** - * Checks whether a backup file exists for the given database file. If it exists, it is checked whether it is - * newer and different from the original. - * - * In case a discarded file is present, the method also returns false, See also {@link #discardBackup(Path)}. - * - * @param originalPath Path to the file a backup should be checked for. Example: jabref.bib. - * - * @return true if backup file exists AND differs from originalPath. false is the - * "default" return value in the good case. In case a discarded file exists, false is returned, too. - * In the case of an exception true is returned to ensure that the user checks the output. - */ - public static boolean backupGitDiffers(Path originalPath, Path backupDir) { - //à implementer + private void startBackupTask(Path backupDir) { + executor.scheduleAtFixedRate( + () -> { + try { + performBackup(backupDir); + } catch (IOException | GitAPIException e) { + LOGGER.error("Error during backup", e); + } + }, + DELAY_BETWEEN_BACKUP_ATTEMPTS_IN_SECONDS, + DELAY_BETWEEN_BACKUP_ATTEMPTS_IN_SECONDS, + TimeUnit.SECONDS); + } + + private void performBackup(Path backupDir) throws IOException, GitAPIException { + if (!needsBackup) { + return; } - return getLatestBackupPath(originalPath, backupDir).map(latestBackupPath -> { - FileTime latestBackupFileLastModifiedTime; - try { - latestBackupFileLastModifiedTime = Files.getLastModifiedTime(latestBackupPath); - } catch (IOException e) { - LOGGER.debug("Could not get timestamp of backup file {}", latestBackupPath, e); - // If we cannot get the timestamp, we do show any warning - return false; - } - FileTime currentFileLastModifiedTime; - try { - currentFileLastModifiedTime = Files.getLastModifiedTime(originalPath); - } catch (IOException e) { - LOGGER.debug("Could not get timestamp of current file file {}", originalPath, e); - // If we cannot get the timestamp, we do show any warning - return false; - } - if (latestBackupFileLastModifiedTime.compareTo(currentFileLastModifiedTime) <= 0) { - // Backup is older than current file - // We treat the backup as non-different (even if it could differ) - return false; - } - try { - boolean result = Files.mismatch(originalPath, latestBackupPath) != -1L; - if (result) { - LOGGER.info("Backup file {} differs from current file {}", latestBackupPath, originalPath); - } - return result; - } catch (IOException e) { - LOGGER.debug("Could not compare original file and backup file.", e); - // User has to investigate in this case - return true; - } - }).orElse(false); + + // Add and commit changes + git.add().addFilepattern(".").call(); + RevCommit commit = git.commit().setMessage("Backup at " + System.currentTimeMillis()).call(); + LOGGER.info("Committed backup: {}", commit.getId()); + + // Reset the backup flag + this.needsBackup = false; } - /** - * Restores the backup file by copying and overwriting the original one. - * - * @param originalPath Path to the file which should be equalized to the backup file. - */ + public static void restoreBackup(Path originalPath, Path backupDir) { - /** - * à implementer - * */ + try { + Git git = Git.open(backupDir.toFile()); + git.checkout().setName("HEAD").call(); + LOGGER.info("Restored backup from Git repository"); + } catch (IOException | GitAPIException e) { + LOGGER.error("Error while restoring the backup", e); + } + } + + private void shutdown(Path backupDir, boolean createBackup) { + changeFilter.unregisterListener(this); + changeFilter.shutdown(); + executor.shutdown(); + + if (createBackup) { + try { + performBackup(backupDir); + } catch (IOException | GitAPIException e) { + LOGGER.error("Error during shutdown backup", e); + } + } } } diff --git a/src/main/java/org/jabref/gui/autosaveandbackup/BackupManager.java b/src/main/java/org/jabref/gui/autosaveandbackup/BackupManager.java index 849ecbccac71..acae02c01c8b 100644 --- a/src/main/java/org/jabref/gui/autosaveandbackup/BackupManager.java +++ b/src/main/java/org/jabref/gui/autosaveandbackup/BackupManager.java @@ -236,9 +236,7 @@ void performBackup(Path backupPath) { LOGGER.error("Could not delete backup file {}", oldestBackupFile, e); } } - //l'ordre dans lequel les entrées BibTeX doivent être écrites dans le fichier de sauvegarde. - // Si l'utilisateur a trié la table d'affichage des entrées dans JabRef, cet ordre est récupéré. - // Sinon, un ordre par défaut est utilisé. + // code similar to org.jabref.gui.exporter.SaveDatabaseAction.saveDatabase SelfContainedSaveOrder saveOrder = bibDatabaseContext .getMetaData().getSaveOrder() @@ -258,10 +256,6 @@ void performBackup(Path backupPath) { } }) .orElse(SaveOrder.getDefaultSaveOrder()); - - //Elle configure la sauvegarde, en indiquant qu'aucune sauvegarde supplémentaire (backup) ne doit être créée, - // que l'ordre de sauvegarde doit être celui défini, et que les entrées doivent être formatées selon les préférences - // utilisateur. SelfContainedSaveConfiguration saveConfiguration = (SelfContainedSaveConfiguration) new SelfContainedSaveConfiguration() .withMakeBackup(false) .withSaveOrder(saveOrder) @@ -269,15 +263,13 @@ void performBackup(Path backupPath) { // "Clone" the database context // We "know" that "only" the BibEntries might be changed during writing (see [org.jabref.logic.exporter.BibDatabaseWriter.savePartOfDatabase]) - //Chaque entrée BibTeX (comme un article, livre, etc.) est clonée en utilisant la méthode clone(). - // Cela garantit que les modifications faites pendant la sauvegarde n'affecteront pas l'entrée originale. List list = bibDatabaseContext.getDatabase().getEntries().stream() .map(BibEntry::clone) .map(BibEntry.class::cast) .toList(); BibDatabase bibDatabaseClone = new BibDatabase(list); BibDatabaseContext bibDatabaseContextClone = new BibDatabaseContext(bibDatabaseClone, bibDatabaseContext.getMetaData()); - //Elle définit l'encodage à utiliser pour écrire le fichier. Cela garantit que les caractères spéciaux sont bien sauvegardés. + Charset encoding = bibDatabaseContext.getMetaData().getEncoding().orElse(StandardCharsets.UTF_8); // We want to have successful backups only // Thus, we do not use a plain "FileWriter", but the "AtomicFileWriter" @@ -343,7 +335,7 @@ public synchronized void listen(@SuppressWarnings("unused") BibDatabaseContextCh } private void startBackupTask(Path backupDir) { - fillQueue(backupDir);//remplie backupFilesQueue les files .sav de le meme bibl + fillQueue(backupDir); executor.scheduleAtFixedRate( // We need to determine the backup path on each action, because we use the timestamp in the filename @@ -352,8 +344,7 @@ private void startBackupTask(Path backupDir) { DELAY_BETWEEN_BACKUP_ATTEMPTS_IN_SECONDS, TimeUnit.SECONDS); } -//La méthode fillQueue(backupDir) est définie dans le code et son rôle est de lister et d'ajouter -// les fichiers de sauvegarde existants dans une file d'attente, + private void fillQueue(Path backupDir) { if (!Files.exists(backupDir)) { return; @@ -364,7 +355,7 @@ private void fillQueue(Path backupDir) { try { List allSavFiles = Files.list(backupDir) // just list the .sav belonging to the given targetFile - .filter(p -> p.getFileName().toString().startsWith(prefix))//tous les files .sav commencerait par ce prefix + .filter(p -> p.getFileName().toString().startsWith(prefix)) .sorted().toList(); backupFilesQueue.addAll(allSavFiles); } catch (IOException e) {