diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2b2dfcb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: java + +jdk: + - oraclejdk8 \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..84889f6 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,10 @@ +pipeline { + agent any + stages { + stage('build') { + steps { + sh './gradlew build' + } + } + } +} diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..42d59f7 --- /dev/null +++ b/README.txt @@ -0,0 +1,45 @@ +DataManager - класс-прослойка между логикой системы контроля версий и файловой системой. Умеет создавать всякие служебные папки, сохранять в них копии файлов, служебную информацию; очищать рабочую копию, откатываться к заданной версии. + +LiteVCS - основной класс, с которым будет работать пользователь библиотеки. Описывает логику работы. Большинство методов реализуют какую-то конкретную команду из списка ниже. Активно взаимодействует с DataManager. + +Algorithms - класс со статическими методами реализующими алгоритм поиска LCA с помощью двоичных подъёмов. Также может найти всех предков заданной версии. Активно взаимодействует с VersionNode, DataManager-ом и больше ни с чем. + +ConsoleWorker - консольное приложение, которое парсит команду и вызывает метод LiteVCS с нужными аргументами. + +Примитивные классы: +ContentDescriptor хранит мапу из относительных путей в id версий фйалов. По id ContentDescriptor-а можно днлать checkout. Также используется для хранения Stage - списка версий файлов, которые надо добавить к следующему коммиту. + +Commit хранит имя автора коммита, время его создания, комментарий к коммиту и id ContentDescriptor-а который задаёт состояние файлов в репозитории. + +VersionNode хранит id коммита, с которым она ассоциирована, а также таблицу предков и глубину в дереве версий, используемых алгоритмом двоичного подъёма. Отделён от Commit чтобы облегчить VersionNode, которые будут в большом количестве читаться в процессе поиска общего предка. + +Branch хранит id VersionNode, которая считается полседней версией этой ветки. Также ветка имеет имя. + +Header хранит имя последнего указанного автора (через hello) (это имя подставляется в создаваемые коммиты) и имя активной ветки. + +Stage хранит информацию об изменениях, которые войдут в следующий коммит. + + + + +Список команд: + init - Инициализирует репозиторий в текущей папке + add [файл] - Сохраняет копию текущего состояния файла, чтобы добавить его к ближайшему коммиту + commit [сообщение] - Делает коммит в текущую ветку, тем самым подтверждая все изменения + checkout [id of content descriptor] - id можно узнать через logs или подсмотрев в папку .liteVCS/descriptors. + очищает рабочую папку, а затем подгружает прописанные в дескрипторе файлы. + clean - Очищает рабочую папку, удаляя всё, кроме папки .liteVCS и файлов, входивших в последний коммит или изменения которых уже добавлены в Stage + status - Показывает изменения, находящиеся в Stage, для каждого файла из рабочей папки сообщает статус (CHANGED - файл отличается от версии в последнем коммите и в Stage-е; NOT_CHANGED; UNKNOWN - не отслеживается репозиторием), а также перечисляет файлы, которые из папки пропали, но не были удалены через репозиторий. + create_branch [название] - Создёт новую ветку, ответвляющуюся от головы текущей ветки + remove_branch [название] - Удаляет ветку + switch_branch [название] - Переключается на другую ветку. Предварительно необходимо закоммитить все накопленные изменения. + merge_branch [название] [сообщение] - Если нет неразрешимых конфликтов (один и тот же файл изменили по разному в разных ветках), то + добавляет изменения из указанной ветки в текущую, создавая коммит с указанным сообщением. + reset [файл] - Откатывает состояние файла к последнему коммиту. Удаляет иформацию об изменении этого файла из Stage + uninstall - Удаляет репозиторий + logs [число] - Показывает последние [число] коммитов в текующей ветке + hello [имя] - Задаёт имя пользователя. + remove [файл] - Удаляет файл из рабочей папки, записывает в Stage информацию о том, что в следующем коммите надо этот файл перестать отслеживать + + + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..93e839e --- /dev/null +++ b/build.gradle @@ -0,0 +1,25 @@ +group 'my_projects' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +repositories { + mavenCentral() +} + +jar { + from {configurations.compile.collect { it.isDirectory() ? it : zipTree(it)} } + manifest { attributes 'Main-Class': 'ru.spbau.lobanov.liteVCS.ConsoleWorker' } +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.11' + // https://mvnrepository.com/artifact/com.google.guava/guava + compile group: 'com.google.guava', name: 'guava', version: '21.0' + + compile group: 'org.jetbrains', name: 'annotations', version: '13.0' + + // https://mvnrepository.com/artifact/org.mockito/mockito-all + compile group: 'org.mockito', name: 'mockito-all', version: '1.10.19' + +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..6ffa237 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cf2daba --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Mar 19 12:08:40 MSK 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..9aa616c --- /dev/null +++ b/gradlew @@ -0,0 +1,169 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..55e76f1 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'my_svn' + diff --git a/src/main/java/ru/spbau/lobanov/liteVCS/ConsoleWorker.java b/src/main/java/ru/spbau/lobanov/liteVCS/ConsoleWorker.java new file mode 100644 index 0000000..827164a --- /dev/null +++ b/src/main/java/ru/spbau/lobanov/liteVCS/ConsoleWorker.java @@ -0,0 +1,195 @@ +package ru.spbau.lobanov.liteVCS; + +import ru.spbau.lobanov.liteVCS.logic.DataManager.BrokenFileException; +import ru.spbau.lobanov.liteVCS.logic.DataManager.LostFileException; +import ru.spbau.lobanov.liteVCS.logic.DataManager.RecreatingRepositoryException; +import ru.spbau.lobanov.liteVCS.logic.DataManager.RepositoryNotInitializedException; +import ru.spbau.lobanov.liteVCS.logic.LiteVCS; +import ru.spbau.lobanov.liteVCS.logic.LiteVCS.*; +import ru.spbau.lobanov.liteVCS.logic.Logging; +import ru.spbau.lobanov.liteVCS.logic.VersionControlSystemException; +import ru.spbau.lobanov.liteVCS.primitives.Commit; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +public class ConsoleWorker { + + private static final String COMMIT_PLACE_HOLDER = "\"%s\" by %s (node: %s)\n"; + private static final String STATUS_PLACE_HOLDER = " \"%s\" status=%s\n"; + + private final LiteVCS liteVCS; + + public ConsoleWorker(LiteVCS liteVCS) { + this.liteVCS = liteVCS; + } + + /** + * Sugar to simplify checking count of arguments + * + * @param size expected arguments number + * @param args array of arguments + * @throws WrongNumberArgumentsException if length of array isn't equal to expected value + */ + private static void checkArguments(int size, String[] args) throws WrongNumberArgumentsException { + if (args.length != size) { + throw new WrongNumberArgumentsException("Expected " + size + " arguments"); + } + } + + public void execute(String command, String[] args) throws VersionControlSystemException, + WrongNumberArgumentsException, IOException, UnknownCommandException { + switch (command) { + case "init": + checkArguments(0, args); + liteVCS.init(); + break; + case "add": + checkArguments(1, args); + liteVCS.add(args[0]); + break; + case "commit": + checkArguments(1, args); + liteVCS.commit(args[0]); + break; + case "checkout": + checkArguments(1, args); + liteVCS.checkout(args[0]); + break; + case "clean": + checkArguments(0, args); + liteVCS.clean(); + break; + case "status": + checkArguments(0, args); + printStatus(liteVCS.stageStatus(), liteVCS.workingCopyStatus()); + break; + case "create_branch": + checkArguments(1, args); + liteVCS.createBranch(args[0]); + break; + case "remove_branch": + checkArguments(1, args); + liteVCS.removeBranch(args[0]); + break; + case "switch_branch": + checkArguments(1, args); + liteVCS.switchBranch(args[0]); + break; + case "merge_branch": + checkArguments(2, args); + liteVCS.mergeBranch(args[0], args[1]); + break; + case "reset": + checkArguments(1, args); + liteVCS.reset(args[0]); + break; + case "remove": + checkArguments(1, args); + liteVCS.remove(args[0]); + break; + case "uninstall": + checkArguments(0, args); + liteVCS.uninstall(); + break; + case "logs": + checkArguments(1, args); + printHistory(liteVCS.history(args[0])); + break; + case "hello": + checkArguments(1, args); + liteVCS.hello(args[0]); + break; + default: + throw new UnknownCommandException(command); + } + } + + public static void main(String... args) { + try { + Logging.setupLogging(); + } catch (Logging.LoggingException e) { + System.out.println("Logging error: " + e.getMessage()); + System.out.println("Original message: " + e.getCause().getMessage()); + return; + } + if (args.length == 0) { + System.out.println("Error: empty command"); + return; + } + String[] functionArgs = new String[args.length - 1]; + System.arraycopy(args, 1, functionArgs, 0, functionArgs.length); + String targetPath = Paths.get(System.getProperty("user.dir")).toString(); + ConsoleWorker consoleWorker = new ConsoleWorker(new LiteVCS(targetPath)); + try { + consoleWorker.execute(args[0], functionArgs); + } catch (ConflictMergeException e) { + System.out.println("Conflicts were found:"); + for (String path : e.getConflicts()) { + System.out.println(" " + path); + } + } catch (LostFileException e) { + System.out.println("Error: Looks, like important file disappeared: " + e.getExpectedFile().toString()); + } catch (BrokenFileException e) { + System.out.println("Error: Looks, like file was badly changed: " + e.getBrokenFile().toString()); + } catch (UnknownBranchException e) { + System.out.println("Error: branch doesn't exist"); + } catch (UncommittedChangesException e) { + System.out.println("Error: commit or reset changes first"); + } catch (UnknownCommandException e) { + System.out.println("Error: command doesn't exist"); + } catch (WrongNumberArgumentsException e) { + System.out.println("Error: wrong number of arguments"); + } catch (RepositoryNotInitializedException e) { + System.out.println("Error: Looks, like repository wasn't already created"); + } catch (RemoveActiveBranchException e) { + System.out.println("Error: you cant remove active branch"); + } catch (LiteVCS.IllegalBranchToMergeException e) { + System.out.println("Error: Looks, like you tried to branch with it-self"); + } catch (RecreatingRepositoryException e) { + System.out.println("Error: Looks, like you tried to create repository second time"); + } catch (Throwable e) { + System.out.println("Error: " + e.getMessage()); + } + } + + private static void printHistory(List commits) { + System.out.println("Local history:"); + for (int i = commits.size() - 1; i >= 0; i--) { + Commit commit = commits.get(i); + System.out.printf(COMMIT_PLACE_HOLDER, commit.getCommitMessage(), commit.getAuthor(), + commit.getContentDescriptorID()); + } + } + + + + private static void printStatus(Map stageStatus, Map workingCopyStatus) { + System.out.println("---------------------------------------------"); + System.out.println("Status of repository:"); + System.out.println(" Stage:"); + for (Entry entry : stageStatus.entrySet()) { + System.out.printf(STATUS_PLACE_HOLDER, entry.getKey(), entry.getValue()); + } + System.out.println(" Working copy:"); + for (Entry entry : workingCopyStatus.entrySet()) { + System.out.printf(STATUS_PLACE_HOLDER, entry.getKey(), entry.getValue()); + } + System.out.println("---------------------------------------------"); + } + + public static class WrongNumberArgumentsException extends Exception { + WrongNumberArgumentsException(String message) { + super(message); + } + } + + public static class UnknownCommandException extends Exception { + UnknownCommandException(String message) { + super(message); + } + } +} diff --git a/src/main/java/ru/spbau/lobanov/liteVCS/logic/Algorithms.java b/src/main/java/ru/spbau/lobanov/liteVCS/logic/Algorithms.java new file mode 100644 index 0000000..206a3aa --- /dev/null +++ b/src/main/java/ru/spbau/lobanov/liteVCS/logic/Algorithms.java @@ -0,0 +1,205 @@ +package ru.spbau.lobanov.liteVCS.logic; +import org.jetbrains.annotations.NotNull; + +import ru.spbau.lobanov.liteVCS.logic.DataManager.BrokenFileException; +import ru.spbau.lobanov.liteVCS.logic.DataManager.LostFileException; +import ru.spbau.lobanov.liteVCS.primitives.VersionNode; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +class Algorithms { + + private static final int CACHE_SIZE_LIMIT = 16; + private static final int LONGEST_JUMP_LENGTH = 1 << (CACHE_SIZE_LIMIT - 1); + + /** + * Special method which create root VersionNode. + * It's important, because we have to know its own + * id before create instance + * + * @param expectedID id of parent of VersionNode, which will be created + * @param commitID id of commit, associated with VersionNode which will be created + * @return created versionNode + */ + @NotNull + static VersionNode createRootNode(@NotNull String expectedID, @NotNull String commitID) { + String[] parentsTable = new String[CACHE_SIZE_LIMIT]; + Arrays.fill(parentsTable, expectedID); + return new VersionNode(commitID, 1, parentsTable); + } + + /** + * Special method which create VersionNode. + * It's important, because we have to calculate table of parents, + * before create VersionNode + * + * @param commitID id of commit, associated with VersionNode which will be created + * @param parentID id of parent of VersionNode, which will be created + * @param manager dataManager, which provides access to the files + * @return created versionNode + * @throws LostFileException if file contained one of interesting Node was corrupted + * @throws BrokenFileException if file contained one of interesting Node was not found + */ + @NotNull + static VersionNode createVersionNode(@NotNull String commitID, @NotNull String parentID, + @NotNull DataManager manager) throws LostFileException, BrokenFileException { + String[] parentsTable = new String[CACHE_SIZE_LIMIT]; + parentsTable[0] = parentID; + for (int i = 1; i < CACHE_SIZE_LIMIT; i++) { + parentsTable[i] = getParent(parentsTable[i - 1], i - 1, manager); + } + VersionNode parent = manager.fetchVersionNode(parentID); + return new VersionNode(commitID, parent.getDeepLevel() + 1, parentsTable); + } + + /** + * Method which go up from node and write down its parents + * + * @param versionID id of interesting node + * @param limit limit of recorded ancestors + * @param dataManager dataManager, which provides access to the files + * @return List of closed parents + * @throws LostFileException if file contained one of interesting Node was corrupted + * @throws BrokenFileException if file contained one of interesting Node was not found + */ + @NotNull + static List getAllParents(@NotNull String versionID, int limit, + @NotNull DataManager dataManager) throws BrokenFileException, LostFileException { + String currentVersionID = versionID; + ArrayList list = new ArrayList<>(); + for (int i = 0; i < limit; i++) { + VersionNode currentNode = dataManager.fetchVersionNode(currentVersionID); + list.add(currentNode); + if (currentVersionID.equals(currentNode.getParentsTable()[0])) { + break; + } + currentVersionID = currentNode.getParentsTable()[0]; + } + return list; + } + + /** + * Method which find LCA of given nodes by algorithm + * of binary expansion. In usual case, if deep of nodes + * lower than 2^CACHE_SIZE_LIMIT it work O(log(deep)) times + * + * @param nodeID1 id of one interesting node + * @param nodeID2 id of another interesting node + * @param manager dataManager, which provides access to the files + * @return id of LCA-node + * @throws LostFileException if file contained one of interesting Node was corrupted + * @throws BrokenFileException if file contained one of interesting Node was not found + */ + @NotNull + static String findLowestCommonAncestor(@NotNull String nodeID1, @NotNull String nodeID2, + @NotNull DataManager manager) throws LostFileException, BrokenFileException { + String currentNodeID1 = nodeID1; + String currentNodeID2 = nodeID2; + int deepDelta = getDeepLevel(nodeID1, manager) - getDeepLevel(nodeID2, manager); + if (deepDelta > 0) { + currentNodeID1 = jump(currentNodeID1, deepDelta, manager); + } else if (deepDelta < 0) { + currentNodeID2 = jump(currentNodeID2, -deepDelta, manager); + } + if (currentNodeID1.equals(currentNodeID2)) { + return currentNodeID1; + } + while (!isAncestorsEqual(currentNodeID1, currentNodeID2, CACHE_SIZE_LIMIT - 1, manager)) { + currentNodeID1 = getParent(currentNodeID1, CACHE_SIZE_LIMIT - 1, manager); + currentNodeID2 = getParent(currentNodeID2, CACHE_SIZE_LIMIT - 1, manager); + } + for (int level = CACHE_SIZE_LIMIT - 1; level >= 0; level--) { + if (!isAncestorsEqual(currentNodeID1, currentNodeID2, level, manager)) { + currentNodeID1 = getParent(currentNodeID1, level, manager); + currentNodeID2 = getParent(currentNodeID2, level, manager); + } + } + return getParent(currentNodeID1, 0, manager); + } + + /** + * Check if parents, remote from interesting nodes on + * 2^level distance, are the same + * Expected, that nodes has the same deep level + * + * @param nodeID1 id of one interesting node + * @param nodeID2 id of another interesting node + * @param level number, which define distance between nodes + * and their interesting parents + * @param manager dataManager, which provides access to the files + * @return true, if parents are the same + * @throws BrokenFileException if file contained one of interesting Node was corrupted + * @throws LostFileException if file contained one of interesting Node was not found + */ + private static boolean isAncestorsEqual(@NotNull String nodeID1, @NotNull String nodeID2, int level, + @NotNull DataManager manager) throws BrokenFileException, LostFileException { + String ancestorID1 = manager.fetchVersionNode(nodeID1).getParentsTable()[level]; + String ancestorID2 = manager.fetchVersionNode(nodeID2).getParentsTable()[level]; + return ancestorID1.equals(ancestorID2); + } + + /** + * Method which find parent of interesting node, located at length levels + * upper or the root of tree if such parent doesn't exist + * It works O(length / (2^CACHE_SIZE_LIMIT) + log(length)) time. + * + * @param nodeID id of interesting node + * @param length expected distance between interesting node and its interesting parent + * @param manager dataManager, which provides access to the files + * @return id of interesting parent + * @throws BrokenFileException if file contained interesting Node was corrupted + * @throws LostFileException if file contained interesting Node was not found + */ + @NotNull + private static String jump(@NotNull String nodeID, int length, + @NotNull DataManager manager) throws LostFileException, BrokenFileException { + String currentNodeID = nodeID; + int residualLength = length; + while (residualLength > LONGEST_JUMP_LENGTH) { + currentNodeID = getParent(currentNodeID, CACHE_SIZE_LIMIT - 1, manager); + residualLength -= LONGEST_JUMP_LENGTH; + } + for (int i = CACHE_SIZE_LIMIT - 1; i >= 0; i--) { + int jumpLength = 1 << i; + if (residualLength >= jumpLength) { + currentNodeID = getParent(currentNodeID, i, manager); + residualLength -= jumpLength; + } + } + return currentNodeID; + } + + /** + * Sugar to simplify getting parents of VersionNode by id + * Its return parent of interesting node, located at (2^level) levels + * upper or the root of tree if such parent doesn't exist + * + * @param nodeID id of interesting node + * @param level distance between interesting node and returned parent will be 2^level + * @param manager dataManager, which provides access to the files + * @return id of interesting parent + * @throws BrokenFileException if file contained interesting Node was corrupted + * @throws LostFileException if file contained interesting Node was not found + */ + @NotNull + private static String getParent(@NotNull String nodeID, int level, + @NotNull DataManager manager) throws BrokenFileException, LostFileException { + return manager.fetchVersionNode(nodeID).getParentsTable()[level]; + } + + /** + * Sugar to simplify getting the deep level by VersionNode id + * + * @param nodeID id of interesting node + * @param manager dataManager, which provides access to the files + * @return deep level of VersionNode associated this nodeID + * @throws BrokenFileException if file contained interesting Node was corrupted + * @throws LostFileException if file contained interesting Node was not found + */ + private static int getDeepLevel(@NotNull String nodeID, @NotNull DataManager manager) throws BrokenFileException, + LostFileException { + return manager.fetchVersionNode(nodeID).getDeepLevel(); + } +} diff --git a/src/main/java/ru/spbau/lobanov/liteVCS/logic/DataManager.java b/src/main/java/ru/spbau/lobanov/liteVCS/logic/DataManager.java new file mode 100644 index 0000000..a80e67d --- /dev/null +++ b/src/main/java/ru/spbau/lobanov/liteVCS/logic/DataManager.java @@ -0,0 +1,572 @@ +package ru.spbau.lobanov.liteVCS.logic; + +import com.google.common.hash.Hashing; +import com.google.common.io.Files; +import org.jetbrains.annotations.NotNull; +import ru.spbau.lobanov.liteVCS.primitives.*; + +import java.io.*; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.logging.Logger; + +/** + * This class is adapter between logic part of LiteVCS + * and data storage + */ +public class DataManager { + + private static final Logger logger = Logger.getLogger(DataManager.class.getName()); + + private static final String ROOT_DIRECTORY_NAME = ".liteVCS"; + private static final String PATH_TO_VERSIONS_FILES = concat(ROOT_DIRECTORY_NAME, "versions"); + private static final String PATH_TO_CONTENT_DESCRIPTORS_FILES = concat(ROOT_DIRECTORY_NAME, "descriptors"); + private static final String PATH_TO_COMMITS_FILES = concat(ROOT_DIRECTORY_NAME, "commits"); + private static final String PATH_TO_SAVED_FILES = concat(ROOT_DIRECTORY_NAME, "files"); + private static final String PATH_TO_BRANCHES = concat(ROOT_DIRECTORY_NAME, "branches"); + private static final String ROOT_VERSION_NODE_ID = "root"; + private static final String PATH_TO_STAGE = concat(ROOT_DIRECTORY_NAME, "stage.lVCS"); + private static final String PATH_TO_HEADER = concat(ROOT_DIRECTORY_NAME, "header.lVCS"); + + @NotNull + private final String workingDirectory; + + DataManager(@NotNull String workingDirectory) { + this.workingDirectory = workingDirectory; + } + + /** + * Method which init repository: create folders and initial files + * + * @throws RecreatingRepositoryException if repository was already created + */ + void initRepository() throws RecreatingRepositoryException, IOException { + File rootDirectory = Paths.get(workingDirectory, DataManager.ROOT_DIRECTORY_NAME).toFile(); + if (rootDirectory.exists()) { + throw new RecreatingRepositoryException("Repository was already created here:" + workingDirectory); + } + boolean success = Paths.get(workingDirectory, DataManager.PATH_TO_COMMITS_FILES).toFile().mkdirs() && + Paths.get(workingDirectory, DataManager.PATH_TO_CONTENT_DESCRIPTORS_FILES).toFile().mkdirs() && + Paths.get(workingDirectory, DataManager.PATH_TO_SAVED_FILES).toFile().mkdirs() && + Paths.get(workingDirectory, DataManager.PATH_TO_VERSIONS_FILES).toFile().mkdirs() && + Paths.get(workingDirectory, DataManager.PATH_TO_BRANCHES).toFile().mkdirs(); + if (!success) { + throw new IOException("Unexpected error during directories creating"); + } + try { + String initialDescriptorID = addContentDescriptor(ContentDescriptor.EMPTY); + String initialCommitID = addCommit(new Commit(initialDescriptorID, "Initial commit", + System.currentTimeMillis(), "lVCS")); + VersionNode start = Algorithms.createRootNode(DataManager.ROOT_VERSION_NODE_ID, initialCommitID); + writeObject(Paths.get(workingDirectory, PATH_TO_VERSIONS_FILES, ROOT_VERSION_NODE_ID), start); + Branch master = new Branch(DataManager.ROOT_VERSION_NODE_ID, "master"); + addBranch(master); + Header header = new Header("Unknown", master.getName()); + putHeader(header); + putStage(Stage.EMPTY); + } catch (RepositoryNotInitializedException e) { + throw new Error("Unexpected error during repository initialization"); + } + } + + /** + * Method allow to find and load VersionNode by id + * + * @param id identifier of VersionNode + * @return loaded VersionNode + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + */ + @NotNull + VersionNode fetchVersionNode(@NotNull String id) throws LostFileException, BrokenFileException { + return readObject(Paths.get(workingDirectory, PATH_TO_VERSIONS_FILES, id), VersionNode.class); + } + + /** + * Method allow to save VersionNode in file system + * + * @param versionNode object to save + * @return generated id, by which you can fetch that object late + * @throws RepositoryNotInitializedException if file creating failed + */ + @NotNull + String addVersionNode(@NotNull VersionNode versionNode) throws RepositoryNotInitializedException { + String id = createUniqueID(PATH_TO_VERSIONS_FILES); + writeObject(Paths.get(workingDirectory, PATH_TO_VERSIONS_FILES, id), versionNode); + return id; + } + + /** + * Method allow to find and load ContentDescriptor by id + * + * @param id identifier of ContentDescriptor + * @return loaded ContentDescriptor + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + */ + @NotNull + ContentDescriptor fetchContentDescriptor(@NotNull String id) throws LostFileException, BrokenFileException { + return readObject(Paths.get(workingDirectory, PATH_TO_CONTENT_DESCRIPTORS_FILES, id), ContentDescriptor.class); + } + + /** + * Method allow to save ContentDescriptor in file system + * + * @param contentDescriptor object to save + * @return generated id, by which you can fetch that object late + * @throws RepositoryNotInitializedException if file creating failed + */ + @NotNull + String addContentDescriptor(@NotNull ContentDescriptor contentDescriptor) throws RepositoryNotInitializedException { + String id = createUniqueID(PATH_TO_CONTENT_DESCRIPTORS_FILES); + writeObject(Paths.get(workingDirectory, PATH_TO_CONTENT_DESCRIPTORS_FILES, id), contentDescriptor); + return id; + } + + /** + * Method allow to find and load Commit by id + * + * @param id identifier of Commit + * @return loaded Commit + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + */ + @NotNull + Commit fetchCommit(@NotNull String id) throws LostFileException, BrokenFileException { + return readObject(Paths.get(workingDirectory, PATH_TO_COMMITS_FILES, id), Commit.class); + } + + /** + * Method allow to save Commit in file system + * + * @param commit object to save + * @return generated id, by which you can fetch that object late + * @throws RepositoryNotInitializedException if file creating failed + */ + @NotNull + String addCommit(@NotNull Commit commit) throws RepositoryNotInitializedException { + String id = createUniqueID(PATH_TO_COMMITS_FILES); + writeObject(Paths.get(workingDirectory, PATH_TO_COMMITS_FILES, id), commit); + return id; + } + + /** + * Method allow to find copy of files by id + * + * @param id identifier of interesting file + * @return link to saved copy + * @throws LostFileException if file contained one of interesting object was corrupted + */ + @NotNull + private File fetchFile(@NotNull String id) throws LostFileException { + File file = Paths.get(workingDirectory, PATH_TO_SAVED_FILES, id).toFile(); + if (!file.exists()) { + throw new LostFileException("File wasn't found:" + file.getName(), null, file); + } + return file; + } + + /** + * Method allow to save copy of file in file system + * + * @param relativePath relative path to file which will bw saved + * @return generated id, by which you can fetch that object late + * @throws RepositoryNotInitializedException if file creating failed + * @throws NonexistentFileAdditionException if file wasn't found + */ + @NotNull + String addFile(@NotNull String relativePath) throws RepositoryNotInitializedException, + NonexistentFileAdditionException { + File file = Paths.get(workingDirectory, relativePath).toFile(); + if (!file.exists() || !file.isFile()) { + throw new NonexistentFileAdditionException("File " + relativePath + " doesn't exist"); + } + String hash = hashFile(relativePath); + File savedCopy = Paths.get(workingDirectory, PATH_TO_SAVED_FILES, hash).toFile(); + try { + if (savedCopy.createNewFile()) { + Files.copy(file, savedCopy); + } + } catch (IOException e) { + throw new RepositoryNotInitializedException("Directory wasn't found:" + PATH_TO_SAVED_FILES, e); + } + return hash; + } + + /** + * Method allow to remove files from working folder + * + * @param relativePath relative path to file which will bw saved + * @throws NonexistentFileDeletionException if target file doesn't exist + */ + void removeFile(String relativePath) throws NonexistentFileDeletionException { + File targetFile = Paths.get(workingDirectory, relativePath).toFile(); + if (!targetFile.isFile() || !targetFile.delete()) { + throw new NonexistentFileDeletionException("File " + relativePath + " doesn't exist"); + } + Path path = Paths.get(relativePath).getParent(); + while (path != null) { + File folder = Paths.get(workingDirectory, path.toString()).toFile(); + File[] files = folder.listFiles(); + if (files == null || files.length != 0 || !folder.delete()) { + break; + } + path = path.getParent(); + } + } + + /** + * Method allow to get list of files, which are in the working folder + * + * @return list of relative paths to every file + * @throws IOException if file creating failed + */ + List workingCopyFiles() throws IOException { + List paths = new ArrayList<>(); + File[] files = Paths.get(workingDirectory).toFile().listFiles(); + if (files == null){ + throw new IOException("Cant get children of folder"); + } + Path mainDirectory = Paths.get(workingDirectory, ROOT_DIRECTORY_NAME); + for (File f : files) { + if (!f.toPath().startsWith(mainDirectory)) { + walkFileTree(f, paths); + } + } + return paths; + } + + private void walkFileTree(File file, List paths) throws IOException { + if (file.isFile()) { + paths.add(Paths.get(workingDirectory).relativize(file.toPath()).toString()); + } else { + File[] files = file.listFiles(); + if (files == null) { + throw new IOException("Cant get children of folder:" + file.toPath()); + } + for (File f : files) { + walkFileTree(f, paths); + } + } + } + + /** + * Method allow to calculate hash of files + * + * @param path relative path to target file + * @return HashCode + */ + String hashFile(String path) { + String hash; + try { + hash = Files.hash(Paths.get(workingDirectory, path).toFile(), Hashing.sha256()).toString() + ".sc"; + } catch (IOException e) { + throw new RuntimeException("Unexpected error during hashing"); + } + return hash; + } + + /** + * Method allow to save branch in file system + * This method doesn't return id because branches are identified by name + * + * @param branch object to save + * @throws RepositoryNotInitializedException if file creating failed + */ + void addBranch(@NotNull Branch branch) throws RepositoryNotInitializedException { + writeObject(Paths.get(workingDirectory, PATH_TO_BRANCHES, branch.getName()), branch); + } + + /** + * Method allow to find and load Branch by name + * + * @param name name of interesting Branch + * @return loaded Branch + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + */ + @NotNull + Branch fetchBranch(@NotNull String name) throws LostFileException, BrokenFileException { + return readObject(Paths.get(workingDirectory, PATH_TO_BRANCHES, name), Branch.class); + } + + /** + * Method allow to check if Branch with such name exist + * + * @param name name of interesting Branch + * @return true if such branch exist, false otherwise + */ + boolean hasBranch(@NotNull String name) { + return Paths.get(workingDirectory, PATH_TO_BRANCHES, name).toFile().exists(); + } + + /** + * Method allow to check if Branch with such name exist + * + * @param name name of interesting Branch + * @throws LostFileException if such branch wasn't found + */ + void removeBranch(@NotNull String name) throws LostFileException { + File file = Paths.get(workingDirectory, PATH_TO_BRANCHES, name).toFile(); + if (!file.delete()) { + throw new LostFileException("File wasn't found", null, file); + } + } + + /** + * Method allow to load Header of repository + * + * @return loaded Header + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + */ + @NotNull + Header getHeader() throws LostFileException, BrokenFileException { + return readObject(Paths.get(workingDirectory, PATH_TO_HEADER), Header.class); + } + + /** + * Method allow to save Header of repository + * + * @param header object to save + * @throws RepositoryNotInitializedException if file saving failed + */ + void putHeader(@NotNull Header header) throws RepositoryNotInitializedException { + writeObject(Paths.get(workingDirectory, PATH_TO_HEADER), header); + } + + /** + * Method allow to load Stage descriptor + * + * @return stage current stage + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + */ + @NotNull + Stage getStage() throws LostFileException, BrokenFileException { + return readObject(Paths.get(workingDirectory, PATH_TO_STAGE), Stage.class); + } + + /** + * Method allow to save Stage + * + * @param stage object to save + * @throws RepositoryNotInitializedException if file saving failed + */ + void putStage(@NotNull Stage stage) throws RepositoryNotInitializedException { + writeObject(Paths.get(workingDirectory, PATH_TO_STAGE), stage); + } + + /** + * Clone file from repository to working copy + * + * @param fileID identifier of saved file + * @param targetPath desired relative path to new copy (including file name) + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws IOException if file creating failed because of File System + */ + void loadFile(@NotNull String fileID, @NotNull String targetPath) throws LostFileException, IOException { + File savedCopy = fetchFile(fileID); + File targetFile = Paths.get(workingDirectory, targetPath).toFile(); + Files.createParentDirs(targetFile); + Files.touch(targetFile); + Files.copy(savedCopy, targetFile); + } + + /** + * Method allow to clear work space be removing all files except ROOT_DIRECTORY + */ + void clearWorkingCopy() { + Path rootDirectory = Paths.get(workingDirectory, ROOT_DIRECTORY_NAME); + File[] files = Paths.get(workingDirectory).toFile().listFiles(); + if (files == null) { + throw new Error("Cant clear Directory:"); + } + for (File file : files) { + if (!file.toPath().startsWith(rootDirectory)) { + if (file.isDirectory()) { + clearDirectory(file); + } + if (!file.delete()) { + throw new Error("Cant remove file: " + file.getAbsolutePath()); + } + } + } + } + + /** + * Method allow to clear work space be removing all files including ROOT_DIRECTORY + * + * @throws RepositoryNotInitializedException if there was no repository + */ + void uninstallRepository() throws RepositoryNotInitializedException { + File rootDirectory = Paths.get(workingDirectory, ROOT_DIRECTORY_NAME).toFile(); + if (!rootDirectory.exists()) { + throw new RepositoryNotInitializedException("Repository wasn't found"); + } + clearDirectory(rootDirectory); + if (!rootDirectory.delete()) { + throw new Error(); + } + + } + + /** + * Method remove all files from directory + * + * @param directory directory to remove + */ + private void clearDirectory(@NotNull File directory) { + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + clearDirectory(file); + } + if (!file.delete()) { + throw new Error("Cant remove file"); + } + } + } + } + + /** + * Method allow you to create such names, + * that files with that names doesn't exist in interesting directory + * + * @param directory tracked folder + * @return such name, that there is no file with that name in that folder + */ + @NotNull + private String createUniqueID(@NotNull String directory) { + Random random = new Random(); + String id; + do { + id = "" + random.nextLong(); + } while (Paths.get(workingDirectory, directory, id).toFile().exists()); + return id; + } + + /** + * Method allow you to load every Serializable object + * from specified files + * + * @param path file, included important information + * @return loaded object + */ + @NotNull + private static T readObject(@NotNull Path path, @NotNull Class expectedType) throws BrokenFileException, + LostFileException { + Object o; + try (FileInputStream fileInputStream = new FileInputStream(path.toFile()); + ObjectInputStream inputStream = new ObjectInputStream(fileInputStream)) { + o = inputStream.readObject(); + } catch (ClassNotFoundException | InvalidClassException | OptionalDataException e) { + throw new BrokenFileException("Cant load data from file: " + path.toString(), e, path.toFile()); + } catch (FileNotFoundException e) { + throw new LostFileException("Found reference to non-existent file: " + path.toString(), e, path.toFile()); + } catch (IOException e) { + throw new Error("Unknown error occurred while reading the file: " + path.toString(), e); + } + if (!expectedType.isInstance(o)) { + throw new BrokenFileException("Unexpected data was found in file: " + path.toString(), path.toFile()); + } + logger.fine("Instance of " + expectedType.getName() + " was successfully loaded"); + return expectedType.cast(o); + } + + /** + * Method allow you to save every Serializable object + * in specified files + * + * @param path path to target file + * @param object object to save + */ + private static void writeObject(@NotNull Path path, @NotNull Object object) + throws RepositoryNotInitializedException { + try (FileOutputStream fileOutputStream = new FileOutputStream(path.toFile()); + ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream)) { + outputStream.writeObject(object); + } catch (FileNotFoundException e) { + throw new RepositoryNotInitializedException("Repository wasn't initialized or was corrupted"); + } catch (IOException e) { + throw new Error("Unknown error occurred while writing the file: " + path.toString(), e); + } + logger.fine("Instance of " + object.getClass().getName() + " was successfully saved"); + } + + /** + * Special method to simplify definition of service paths + * + * @param root main directory + * @param paths relative paths + * @return concatenation of paths + */ + @NotNull + private static String concat(@NotNull String root, @NotNull String... paths) { + return Paths.get(root, paths).toString(); + } + + public static class LostFileException extends VersionControlSystemException { + private final File expectedFile; + + LostFileException(String message, Throwable cause, File expectedFile) { + super(message, cause); + this.expectedFile = expectedFile; + } + + public File getExpectedFile() { + return expectedFile; + } + } + + public static class BrokenFileException extends VersionControlSystemException { + private final File brokenFile; + + BrokenFileException(String message, Throwable cause, File brokenFile) { + super(message, cause); + this.brokenFile = brokenFile; + } + + BrokenFileException(String message, File brokenFile) { + super(message); + this.brokenFile = brokenFile; + } + + public File getBrokenFile() { + return brokenFile; + } + } + + + public static class RepositoryNotInitializedException extends VersionControlSystemException { + RepositoryNotInitializedException(String message) { + super(message); + } + + RepositoryNotInitializedException(String message, Throwable cause) { + super(message, cause); + } + } + + public static class RecreatingRepositoryException extends VersionControlSystemException { + RecreatingRepositoryException(String message) { + super(message); + } + } + + public static class NonexistentFileAdditionException extends VersionControlSystemException { + NonexistentFileAdditionException(String message) { + super(message); + } + } + + public static class NonexistentFileDeletionException extends VersionControlSystemException { + NonexistentFileDeletionException(String message) { + super(message); + } + } +} diff --git a/src/main/java/ru/spbau/lobanov/liteVCS/logic/LiteVCS.java b/src/main/java/ru/spbau/lobanov/liteVCS/logic/LiteVCS.java new file mode 100644 index 0000000..89034a2 --- /dev/null +++ b/src/main/java/ru/spbau/lobanov/liteVCS/logic/LiteVCS.java @@ -0,0 +1,611 @@ +package ru.spbau.lobanov.liteVCS.logic; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import ru.spbau.lobanov.liteVCS.logic.DataManager.*; +import ru.spbau.lobanov.liteVCS.primitives.*; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.logging.Logger; + +import static ru.spbau.lobanov.liteVCS.logic.LiteVCS.FileStatus.*; +import static ru.spbau.lobanov.liteVCS.logic.LiteVCS.StageStatus.*; + +/** + * Special class which provides all the main + * functionality of library by static methods + */ +public class LiteVCS { + + private static final Logger logger = Logger.getLogger(LiteVCS.class.getName()); + + private final DataManager dataManager; + + public LiteVCS(@NotNull String path) { + dataManager = new DataManager(path); + } + + public LiteVCS(@NotNull DataManager dataManager) { + this.dataManager = dataManager; + } + + /** + * This method allows to set author's name. + * That name will be mentioned in following commits. + * + * @param author chosen name + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + * @throws RepositoryNotInitializedException if repository was not initialized + */ + public void hello(@NotNull String author) throws BrokenFileException, + LostFileException, RepositoryNotInitializedException { + String branchName = dataManager.getHeader().getCurrentBranchName(); + dataManager.putHeader(new Header(author, branchName)); + logger.fine("Author's name set (" + author + ")"); + } + + /** + * Wrapper for init-method of DataManager + * + * @throws RecreatingRepositoryException if repository was already created + */ + public void init() throws RecreatingRepositoryException, IOException { + dataManager.initRepository(); + logger.fine("Repository successfully created"); + } + + /** + * Method which add file to stage (list of files to commit). + * + * @param fileName relative path to target file + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + * @throws RepositoryNotInitializedException if repository was not initialized + */ + public void add(@NotNull String fileName) throws RepositoryNotInitializedException, + LostFileException, BrokenFileException, NonexistentFileAdditionException { + Stage stage = dataManager.getStage(); + String fileID = dataManager.addFile(fileName); + Stage updatedStage = stage.change() + .addFile(fileName, fileID) + .build(); + dataManager.putStage(updatedStage); + logger.fine("File " + fileName + " was added to stage area"); + } + + /** + * Method which mark file as removed at stage and remove that file from working directory + * + * @param fileName relative path to target file + * @throws RepositoryNotInitializedException if repository was not initialized + */ + public void remove(@NotNull String fileName) throws RepositoryNotInitializedException, + LostFileException, BrokenFileException, NonexistentFileDeletionException { + Header header = dataManager.getHeader(); + Branch currentBranch = dataManager.fetchBranch(header.getCurrentBranchName()); + VersionNode currentVersion = dataManager.fetchVersionNode(currentBranch.getVersionNodeID()); + Commit lastCommit = dataManager.fetchCommit(currentVersion.getCommitID()); + ContentDescriptor descriptor = dataManager.fetchContentDescriptor(lastCommit.getContentDescriptorID()); + Stage stage = dataManager.getStage(); + if (descriptor.getFiles().containsKey(fileName) || stage.getChangedFiles().containsKey(fileName)) { + Stage updatedStage = stage.change() + .removeFile(fileName) + .build(); + dataManager.putStage(updatedStage); + logger.fine("File " + fileName + " will be finally removed from repository in next commit"); + } + dataManager.removeFile(fileName); + } + + /** + * Method which get list of changed from stage, + * create Commit and add it to head of current branch + * + * @param message text which explain changes which was made in this commit + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + * @throws RepositoryNotInitializedException if repository was not initialized + */ + public void commit(@NotNull String message) throws BrokenFileException, + LostFileException, RepositoryNotInitializedException { + Header header = dataManager.getHeader(); + Branch currentBranch = dataManager.fetchBranch(header.getCurrentBranchName()); + VersionNode currentVersion = dataManager.fetchVersionNode(currentBranch.getVersionNodeID()); + Commit lastCommit = dataManager.fetchCommit(currentVersion.getCommitID()); + ContentDescriptor currentDescriptor = dataManager.fetchContentDescriptor(lastCommit.getContentDescriptorID()); + Stage stage = dataManager.getStage(); + + ContentDescriptor updatedDescriptor = ContentDescriptor.builder() + .addAll(currentDescriptor) + .addAll(stage) + .build(); + String descriptorID = dataManager.addContentDescriptor(updatedDescriptor); + Commit newCommit = new Commit(descriptorID, message, System.currentTimeMillis(), header.getAuthor()); + String commitID = dataManager.addCommit(newCommit); + VersionNode newVersion = Algorithms.createVersionNode(commitID, currentBranch.getVersionNodeID(), dataManager); + String versionID = dataManager.addVersionNode(newVersion); + Branch updatedBranch = new Branch(versionID, currentBranch.getName()); + dataManager.addBranch(updatedBranch); + dataManager.putStage(Stage.EMPTY); + logger.fine("New commit created (descriptor id = " + descriptorID + ")"); + } + + /** + * Method which return list of node's parents sorted by increasing distance + * + * @param lengthLimit limit size of returned List + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + */ + @NotNull + public List history(@NotNull String lengthLimit) + throws BrokenFileException, LostFileException { + int limit; + try { + limit = Integer.parseInt(lengthLimit); + } catch (NumberFormatException e) { + logger.fine("Failed to show history, because length limit has wrong format: " + lengthLimit); + throw new IllegalArgumentException("Cant parse length limit", e); + } + Header header = dataManager.getHeader(); + Branch currentBranch = dataManager.fetchBranch(header.getCurrentBranchName()); + List versions = Algorithms.getAllParents(currentBranch.getVersionNodeID(), limit, dataManager); + List commits = new ArrayList<>(); + for (VersionNode versionNode : versions) { + commits.add(dataManager.fetchCommit(versionNode.getCommitID())); + } + logger.fine("History of commit was shown (" + commits.size() + " commits)"); + return commits; + } + + /** + * Method create new Branch which current version + * equal to version of active branch + * + * @param branchName name of new branch + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + * @throws RepositoryNotInitializedException if repository was not initialized + * @throws ConflictNameException if branch with equal name is already exist + */ + public void createBranch(@NotNull String branchName) + throws BrokenFileException, LostFileException, ConflictNameException, RepositoryNotInitializedException { + Header header = dataManager.getHeader(); + Branch currentBranch = dataManager.fetchBranch(header.getCurrentBranchName()); + if (dataManager.hasBranch(branchName)) { + logger.warning("Failed to create new branch because of name collision"); + throw new ConflictNameException("Branch with the same name is already exist"); + } + Branch newBranch = new Branch(currentBranch.getVersionNodeID(), branchName); + dataManager.addBranch(newBranch); + logger.fine("New branch successfully created (name = " + branchName + ")"); + } + + /** + * Method removes record about given branch + * + * @param branchName name of branch to remove + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + * @throws RemoveActiveBranchException if user try to remove active(current) branch + * @throws UnknownBranchException if branch with the same name wasn't found + */ + public void removeBranch(@NotNull String branchName) + throws BrokenFileException, LostFileException, + RemoveActiveBranchException, UnknownBranchException { + Header header = dataManager.getHeader(); + if (header.getCurrentBranchName().equals(branchName)) { + logger.warning("Failed to remove branch because target branch was active"); + throw new RemoveActiveBranchException("Cant remove active branch"); + } + if (!dataManager.hasBranch(branchName)) { + logger.warning("Failed to remove branch because branch with name=" + branchName + " wasn't found"); + throw new UnknownBranchException("Branch to remove doesn't exist"); + } + dataManager.removeBranch(branchName); + } + + /** + * Method merges given branch into current branch + * Conflicts can be resolved only if there is no file, + * which was changed in both branches since theirs last common version + * Stage have to be empty before merging + * + * @param branchName name of branch to remove + * @param message text ehich will be used in commit + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + * @throws RepositoryNotInitializedException if repository was not initialized + * @throws IllegalBranchToMergeException if it's logically impossible to merge thar branches + * @throws UnknownBranchException if branch with the same name wasn't found + * @throws UncommittedChangesException if stage not empty + * @throws ConflictMergeException if unresolvable conflict was found + */ + public void mergeBranch(@NotNull String branchName, @NotNull String message) + throws BrokenFileException, LostFileException, IllegalBranchToMergeException, UnknownBranchException, + UncommittedChangesException, ConflictMergeException, RepositoryNotInitializedException { + Header header = dataManager.getHeader(); + if (header.getCurrentBranchName().equals(branchName)) { + logger.warning("Failed to merge branches. Cant merge branch with itself"); + throw new IllegalBranchToMergeException("Cant merge branch with itself"); + } + if (!dataManager.hasBranch(branchName)) { + logger.warning("Failed to merge branches because one of them (" + branchName + " wasn't found"); + throw new UnknownBranchException("Branch to merge doesn't exist"); + } + if (!dataManager.getStage().isEmpty()) { + logger.warning("Failed to merge branches. Stage wasn't empty"); + throw new UncommittedChangesException("Commit changes before merge"); + } + String activeVersionID = dataManager.fetchBranch(header.getCurrentBranchName()).getVersionNodeID(); + String sideVersionID = dataManager.fetchBranch(branchName).getVersionNodeID(); + String lcaVersionID = Algorithms.findLowestCommonAncestor(activeVersionID, sideVersionID, dataManager); + ContentDescriptor activeContent = toContentDescriptor(activeVersionID); + ContentDescriptor sideContent = toContentDescriptor(sideVersionID); + ContentDescriptor lcaContent = toContentDescriptor(lcaVersionID); + List conflicts = checkConflicts(activeContent, sideContent, lcaContent); + if (!conflicts.isEmpty()) { + logger.warning("Failed to merge branches because of difficult conflicts"); + throw new ConflictMergeException("Conflicts was found", conflicts); + } + ContentDescriptor mergedDescriptor = mergeContent(activeContent, sideContent, lcaContent); + String descriptorID = dataManager.addContentDescriptor(mergedDescriptor); + Commit commit = new Commit(descriptorID, message, System.currentTimeMillis(), header.getAuthor()); + String commitID = dataManager.addCommit(commit); + VersionNode versionNode = Algorithms.createVersionNode(commitID, activeVersionID, dataManager); + String versionNodeID = dataManager.addVersionNode(versionNode); + Branch updatedBranch = new Branch(versionNodeID, header.getCurrentBranchName()); + dataManager.addBranch(updatedBranch); + logger.warning("Branch " + branchName + " was successfully merged into " + header.getCurrentBranchName()); + } + + /** + * Method which merge ContentDescriptors. + * Result of merging - ContentDescriptor contained changes from both descriptors + */ + @NotNull + private static ContentDescriptor mergeContent(@NotNull ContentDescriptor descriptor1, + @NotNull ContentDescriptor descriptor2, + @NotNull ContentDescriptor lcaDescriptor) { + ContentDescriptor.Builder builder = ContentDescriptor.builder(); + Map files1 = descriptor1.getFiles(); + Map files2 = descriptor2.getFiles(); + Map lcaFiles = lcaDescriptor.getFiles(); + + Set allFiles = new HashSet<>(); + allFiles.addAll(files1.keySet()); + allFiles.addAll(files2.keySet()); + + for (String path : allFiles) { + String resultVersion = mergeFileVersions(files1.get(path), files2.get(path), lcaFiles.get(path)); + if (resultVersion != null) { + builder.addFile(path, resultVersion); + } + } + return builder.build(); + } + + /** + * Check if these versions cause unresolvable conflict + */ + private static boolean hasConflict(@Nullable String version1, @Nullable String version2, + @Nullable String lcaVersion) { + return !Objects.equals(version1, version2) && !Objects.equals(version1, lcaVersion) + && !Objects.equals(version2, lcaVersion); + } + + /** + * Choose result version of file, based on it's version in different VersionNodes and in their LCA + */ + @Nullable + private static String mergeFileVersions(@Nullable String version1, @Nullable String version2, + @Nullable String lcaVersion) { + return Objects.equals(version1, lcaVersion) ? version2 : version1; + } + + /** + * Method which finds all unresolvable conflicts between different versions + * + * @param descriptor1 version of file in one Node + * @param descriptor2 version of file in another Node + * @param lcaDescriptor version of file in their LCA + */ + @NotNull + private static List checkConflicts(@NotNull ContentDescriptor descriptor1, + @NotNull ContentDescriptor descriptor2, + @NotNull ContentDescriptor lcaDescriptor) { + List conflicts = new ArrayList<>(); + + Map files1 = descriptor1.getFiles(); + Map files2 = descriptor2.getFiles(); + Map lcaFiles = lcaDescriptor.getFiles(); + + Set allFiles = new HashSet<>(); + allFiles.addAll(files1.keySet()); + allFiles.addAll(files2.keySet()); + + Set folders = new HashSet<>(); + + for (String path : allFiles) { + if (hasConflict(files1.get(path), files2.get(path), lcaFiles.get(path))) { + conflicts.add(path); + continue; + } + if (mergeFileVersions(files1.get(path), files2.get(path), lcaFiles.get(path)) == null) { + continue; + } + Path folder = Paths.get(path).getParent(); + while (folder != null) { + folders.add(folder.toString()); + folder = folder.getParent(); + } + } + + for (String path : allFiles) { + if (!hasConflict(files1.get(path), files2.get(path), lcaFiles.get(path)) + && mergeFileVersions(files1.get(path), files2.get(path), lcaFiles.get(path)) != null + && folders.contains(path)) { + conflicts.add(path); + } + } + + return conflicts; + } + + /** + * Sugar to simplify getting ContentDescriptor from ID of VersionNode + * + * @param versionID id of VersionNode + * @return loaded ContentDescriptor + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + */ + @NotNull + private ContentDescriptor toContentDescriptor(@NotNull String versionID) + throws BrokenFileException, LostFileException { + VersionNode versionNode = dataManager.fetchVersionNode(versionID); + Commit commit = dataManager.fetchCommit(versionNode.getCommitID()); + return dataManager.fetchContentDescriptor(commit.getContentDescriptorID()); + } + + @NotNull + private ContentDescriptor getActualDescriptor() throws BrokenFileException, LostFileException { + Header header = dataManager.getHeader(); + Branch currentBranch = dataManager.fetchBranch(header.getCurrentBranchName()); + VersionNode currentVersion = dataManager.fetchVersionNode(currentBranch.getVersionNodeID()); + Commit lastCommit = dataManager.fetchCommit(currentVersion.getCommitID()); + return dataManager.fetchContentDescriptor(lastCommit.getContentDescriptorID()); + } + + /** + * @param branchName name of interesting branch + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + * @throws RepositoryNotInitializedException if repository was not initialized + * @throws SwitchOnCurrentBranchException if you try to switch on your current branch + * @throws UncommittedChangesException if stage isn't empty before change branch + * @throws UnknownBranchException if such branch wasn't found + * @throws IOException in case of some IO problems + */ + public void switchBranch(@NotNull String branchName) + throws BrokenFileException, LostFileException, SwitchOnCurrentBranchException, + UncommittedChangesException, UnknownBranchException, IOException, RepositoryNotInitializedException { + Header header = dataManager.getHeader(); + if (header.getCurrentBranchName().equals(branchName)) { + logger.warning("Failed to switch branches. Target branch is already active"); + throw new SwitchOnCurrentBranchException("This branch is already chosen"); + } + if (!dataManager.getStage().isEmpty()) { + logger.warning("Failed to switch branches. Stage area isn't empty"); + throw new UncommittedChangesException("Commit changes before switch branch"); + } + if (!dataManager.hasBranch(branchName)) { + logger.warning("Failed to switch branches. Branch wasn't found (" + branchName + ")"); + throw new UnknownBranchException("Branch wasn't found"); + } + Branch branch = dataManager.fetchBranch(branchName); + VersionNode versionNode = dataManager.fetchVersionNode(branch.getVersionNodeID()); + Commit lastCommit = dataManager.fetchCommit(versionNode.getCommitID()); + checkout(lastCommit.getContentDescriptorID()); + Header updatedHeader = new Header(header.getAuthor(), branchName); + dataManager.putHeader(updatedHeader); + logger.fine(branchName + " is active branch now"); + } + + /** + * Restore saved copy from current Branch + * + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + * @throws RepositoryNotInitializedException if repository was not initialized + * @throws IOException in case of some IO problems + * @throws UnobservedFileException in case if filename wasn't observed before + */ + public void reset(String filename) throws BrokenFileException, LostFileException, IOException, + RepositoryNotInitializedException, UnobservedFileException { + ContentDescriptor descriptor = getActualDescriptor(); + if (!descriptor.getFiles().containsKey(filename)) { + throw new UnobservedFileException("File " + filename + " have no saved versions"); + } + dataManager.loadFile(descriptor.getFiles().get(filename), filename); + Stage stage = dataManager.getStage(); + if (stage.getChangedFiles().containsKey(filename) || stage.getRemovedFiles().contains(filename)) { + Stage updatedStage = stage.change() + .reset(filename) + .build(); + dataManager.putStage(updatedStage); + logger.fine("Information about " + filename + " was removed from stage"); + } + } + + /** + * Restore saved files from given ContentDescriptor + * + * @param descriptorID id of interesting description + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + * @throws IOException in case of some IO problems + */ + public void checkout(@NotNull String descriptorID) + throws IOException, BrokenFileException, LostFileException { + dataManager.clearWorkingCopy(); + ContentDescriptor contentDescriptor = dataManager.fetchContentDescriptor(descriptorID); + for (Map.Entry file : contentDescriptor.getFiles().entrySet()) { + dataManager.loadFile(file.getValue(), file.getKey()); + } + logger.fine("Checkout to " + descriptorID + " was successfully finished"); + } + + /** + * This method remove all untracked files from working directory + * + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + * @throws IOException in case of some IO problems + * {@link NonexistentFileDeletionException in case if some error occurred during cleaning working copy}; + */ + public void clean() throws IOException, BrokenFileException, LostFileException, NonexistentFileDeletionException { + List paths = dataManager.workingCopyFiles(); + Stage stage = dataManager.getStage(); + ContentDescriptor headVersion = getActualDescriptor(); + for (String path : paths) { + if (!stage.getChangedFiles().containsKey(path) && !headVersion.getFiles().containsKey(path)) { + dataManager.removeFile(path); + } + } + logger.fine("Working directory was successfully cleaned"); + } + + /** + * Remove repository, but doesn't touch files in working copy + * + * @throws RepositoryNotInitializedException if there is nothing to delete + */ + public void uninstall() throws RepositoryNotInitializedException { + dataManager.uninstallRepository(); + } + + /** + * Return information about files in the stage area + * + * @return Map from File name to Status for every file, added to stage + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + */ + public Map stageStatus() throws BrokenFileException, LostFileException { + Stage stage = dataManager.getStage(); + HashMap result = new HashMap<>(); + for (String s : stage.getChangedFiles().keySet()) { + result.put(s, UPDATED); + } + for (String s : stage.getRemovedFiles()) { + result.put(s, REMOVED); + } + return result; + } + + + /** + * Return information about files in the working folder + * + * @return Map from File name to Status for every file belonging to folder or known in last commit + * @throws LostFileException if file contained one of interesting object was corrupted + * @throws BrokenFileException if file contained one of interesting object was not found + */ + public Map workingCopyStatus() throws BrokenFileException, LostFileException, IOException { + List paths = dataManager.workingCopyFiles(); + Stage stage = dataManager.getStage(); + ContentDescriptor headVersion = getActualDescriptor(); + HashMap result = new HashMap<>(); + + ContentDescriptor stagedDescriptor = ContentDescriptor.builder() + .addAll(headVersion) + .addAll(stage) + .build(); + + + Map files = stagedDescriptor.getFiles(); + for (String path : paths) { + if (!files.containsKey(path)) { + result.put(path, UNKNOWN); + continue; + } + String hash = dataManager.hashFile(path); + if (hash.equals(files.get(path))) { + result.put(path, NOT_CHANGED); + } else { + result.put(path, CHANGED); + } + } + for (String path : files.keySet()) { + if (!result.containsKey(path)) { + result.put(path, DISAPPEARED); + } + } + return result; + } + + public static class SwitchOnCurrentBranchException extends VersionControlSystemException { + SwitchOnCurrentBranchException(String message) { + super(message); + } + } + + public static class UncommittedChangesException extends VersionControlSystemException { + UncommittedChangesException(String message) { + super(message); + } + } + + public static class UnknownBranchException extends VersionControlSystemException { + UnknownBranchException(String message) { + super(message); + } + } + + public static class ConflictNameException extends VersionControlSystemException { + ConflictNameException(String message) { + super(message); + } + } + + public static class RemoveActiveBranchException extends VersionControlSystemException { + RemoveActiveBranchException(String message) { + super(message); + } + } + + public static class ConflictMergeException extends VersionControlSystemException { + + private final List conflicts; + + ConflictMergeException(String message, List conflicts) { + super(message); + this.conflicts = Collections.unmodifiableList(conflicts); + } + + public List getConflicts() { + return conflicts; + } + } + + public static class IllegalBranchToMergeException extends VersionControlSystemException { + IllegalBranchToMergeException(String message) { + super(message); + } + } + + public static class UnobservedFileException extends VersionControlSystemException { + public UnobservedFileException(String message) { + super(message); + } + } + + public enum StageStatus {UPDATED, REMOVED} + + public enum FileStatus {CHANGED, DISAPPEARED, UNKNOWN, NOT_CHANGED} +} diff --git a/src/main/java/ru/spbau/lobanov/liteVCS/logic/Logging.java b/src/main/java/ru/spbau/lobanov/liteVCS/logic/Logging.java new file mode 100644 index 0000000..42e4393 --- /dev/null +++ b/src/main/java/ru/spbau/lobanov/liteVCS/logic/Logging.java @@ -0,0 +1,32 @@ +package ru.spbau.lobanov.liteVCS.logic; + +import sun.rmi.runtime.Log; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +public class Logging { + + private static final Path LOG_STORAGE = Paths.get(".liteVCS", "logs"); + + public static void setupLogging() throws LoggingException { + try { + Files.createDirectories(LOG_STORAGE); + LogManager.getLogManager().readConfiguration( + Logging.class.getResourceAsStream("/logging.properties")); + } catch (IOException e) { + throw new LoggingException("Can't init logging", e); + } + Logger.getLogger(Logger.class.getName()).fine("Logging was set up"); + } + + public static class LoggingException extends Exception { + LoggingException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/src/main/java/ru/spbau/lobanov/liteVCS/logic/VersionControlSystemException.java b/src/main/java/ru/spbau/lobanov/liteVCS/logic/VersionControlSystemException.java new file mode 100644 index 0000000..16c9540 --- /dev/null +++ b/src/main/java/ru/spbau/lobanov/liteVCS/logic/VersionControlSystemException.java @@ -0,0 +1,13 @@ +package ru.spbau.lobanov.liteVCS.logic; + +public class VersionControlSystemException extends Exception { + + VersionControlSystemException(String message) { + super(message); + } + + VersionControlSystemException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/ru/spbau/lobanov/liteVCS/primitives/Branch.java b/src/main/java/ru/spbau/lobanov/liteVCS/primitives/Branch.java new file mode 100644 index 0000000..f777a39 --- /dev/null +++ b/src/main/java/ru/spbau/lobanov/liteVCS/primitives/Branch.java @@ -0,0 +1,30 @@ +package ru.spbau.lobanov.liteVCS.primitives; + +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; + +/** + * Special class which represent branch in liteVCS + * It has name and link to VersionNode + * Its easy to save branch in file + */ +public class Branch implements Serializable { + private final String versionNodeID; + private final String name; + + public Branch(@NotNull String versionNodeID, @NotNull String name) { + this.versionNodeID = versionNodeID; + this.name = name; + } + + @NotNull + public String getVersionNodeID() { + return versionNodeID; + } + + @NotNull + public String getName() { + return name; + } +} diff --git a/src/main/java/ru/spbau/lobanov/liteVCS/primitives/Commit.java b/src/main/java/ru/spbau/lobanov/liteVCS/primitives/Commit.java new file mode 100644 index 0000000..e6425d8 --- /dev/null +++ b/src/main/java/ru/spbau/lobanov/liteVCS/primitives/Commit.java @@ -0,0 +1,44 @@ +package ru.spbau.lobanov.liteVCS.primitives; + +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; + +/** + * Special class which represent commits in liteVCS + * It contains message, time of creating, its authors + * name and link to ContentDescriptor which contains information about files + */ +public class Commit implements Serializable { + + private final String contentDescriptorID; + private final String commitMessage; + private final long time; + private final String author; + + public Commit(@NotNull String descriptorID, @NotNull String commitMessage, long time, @NotNull String author) { + this.contentDescriptorID = descriptorID; + this.commitMessage = commitMessage; + this.time = time; + this.author = author; + } + + @NotNull + public String getContentDescriptorID() { + return contentDescriptorID; + } + + @NotNull + public String getCommitMessage() { + return commitMessage; + } + + public long getTime() { + return time; + } + + @NotNull + public String getAuthor() { + return author; + } +} diff --git a/src/main/java/ru/spbau/lobanov/liteVCS/primitives/ContentDescriptor.java b/src/main/java/ru/spbau/lobanov/liteVCS/primitives/ContentDescriptor.java new file mode 100644 index 0000000..9c4efdd --- /dev/null +++ b/src/main/java/ru/spbau/lobanov/liteVCS/primitives/ContentDescriptor.java @@ -0,0 +1,66 @@ +package ru.spbau.lobanov.liteVCS.primitives; + +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + +/** + * Special class which contains information about + * files and theirs actual versions + * Its possible to save it in file + */ +public class ContentDescriptor implements Serializable { + public static final ContentDescriptor EMPTY = new ContentDescriptor(new TreeMap<>()); + + // Map name of file (relative path from working directory) + // to ID of its actual version + private final TreeMap files; + + private ContentDescriptor(@NotNull TreeMap files) { + this.files = files; + } + + @NotNull + public Map getFiles() { + return Collections.unmodifiableMap(files); + } + + @NotNull + public static Builder builder() { + return new Builder(); + } + + /** + * This class should help to create ContentDescriptors + */ + public static class Builder { + private final TreeMap files = new TreeMap<>(); + + @NotNull + public Builder addFile(@NotNull String relativePath, @NotNull String fileID) { + files.put(relativePath, fileID); + return this; + } + + @NotNull + public Builder addAll(@NotNull ContentDescriptor contentDescriptor) { + files.putAll(contentDescriptor.files); + return this; + } + + @NotNull + public Builder addAll(@NotNull Stage stage) { + files.putAll(stage.getChangedFiles()); + files.keySet().removeAll(stage.getRemovedFiles()); + return this; + } + + @NotNull + public ContentDescriptor build() { + return new ContentDescriptor(files); + } + } +} diff --git a/src/main/java/ru/spbau/lobanov/liteVCS/primitives/Header.java b/src/main/java/ru/spbau/lobanov/liteVCS/primitives/Header.java new file mode 100644 index 0000000..9674940 --- /dev/null +++ b/src/main/java/ru/spbau/lobanov/liteVCS/primitives/Header.java @@ -0,0 +1,29 @@ +package ru.spbau.lobanov.liteVCS.primitives; + +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; + +/** + * Special class to save general information in file + * It knows name of current branch and name of author + */ +public class Header implements Serializable { + private final String author; + private final String currentBranchName; + + public Header(@NotNull String author, @NotNull String currentBranchName) { + this.author = author; + this.currentBranchName = currentBranchName; + } + + @NotNull + public String getAuthor() { + return author; + } + + @NotNull + public String getCurrentBranchName() { + return currentBranchName; + } +} diff --git a/src/main/java/ru/spbau/lobanov/liteVCS/primitives/Stage.java b/src/main/java/ru/spbau/lobanov/liteVCS/primitives/Stage.java new file mode 100644 index 0000000..916bda6 --- /dev/null +++ b/src/main/java/ru/spbau/lobanov/liteVCS/primitives/Stage.java @@ -0,0 +1,82 @@ +package ru.spbau.lobanov.liteVCS.primitives; + +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; +import java.util.*; + +public class Stage implements Serializable { + public static final Stage EMPTY = new Stage(new TreeMap<>(), new TreeSet<>()); + + // Map name of file (relative path from working directory) + // to ID of its actual version + private final TreeMap changedFiles; + private final TreeSet removedFiles; + + private Stage(TreeMap changedFiles, TreeSet removedFiles) { + this.changedFiles = changedFiles; + this.removedFiles = removedFiles; + } + + @NotNull + public Map getChangedFiles() { + return Collections.unmodifiableMap(changedFiles); + } + + public TreeSet getRemovedFiles() { + return removedFiles; + } + + public boolean isEmpty() { + return changedFiles.isEmpty() && removedFiles.isEmpty(); + } + + @NotNull + public static Builder builder() { + return new Builder(Collections.emptyMap(), Collections.emptySet()); + } + + @NotNull + public Builder change() { + return new Builder(changedFiles, removedFiles); + } + + + /** + * This class should help to create ContentDescriptors + */ + public static class Builder { + private final TreeMap changedFiles; + private final TreeSet removedFiles; + + private Builder(Map changedFiles, Set removedFiles) { + this.changedFiles = new TreeMap<>(changedFiles); + this.removedFiles = new TreeSet<>(removedFiles); + } + + @NotNull + public Builder addFile(@NotNull String relativePath, @NotNull String fileID) { + changedFiles.put(relativePath, fileID); + removedFiles.remove(relativePath); + return this; + } + + @NotNull + public Builder removeFile(@NotNull String relativePath) { + removedFiles.add(relativePath); + changedFiles.remove(relativePath); + return this; + } + + public Builder reset(String relationPath) { + removedFiles.remove(relationPath); + changedFiles.remove(relationPath); + return this; + } + + @NotNull + public Stage build() { + return new Stage(changedFiles, removedFiles); + } + } +} diff --git a/src/main/java/ru/spbau/lobanov/liteVCS/primitives/VersionNode.java b/src/main/java/ru/spbau/lobanov/liteVCS/primitives/VersionNode.java new file mode 100644 index 0000000..4829071 --- /dev/null +++ b/src/main/java/ru/spbau/lobanov/liteVCS/primitives/VersionNode.java @@ -0,0 +1,47 @@ +package ru.spbau.lobanov.liteVCS.primitives; + +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; + +/** + * Special class which allow to build tree of versions + * Has link to associated Commit + * Commit and VersionNode are separated to facilitate the version tree + */ +public class VersionNode implements Serializable { + + private final String commitID; + + // data for method of binary expansion + private final int deepLevel; + private final String[] parentsTable; + + public VersionNode(@NotNull String commitID, int deepLevel, @NotNull String[] parentsTable) { + this.commitID = commitID; + this.deepLevel = deepLevel; + this.parentsTable = parentsTable; + } + + @NotNull + public String getCommitID() { + return commitID; + } + + public int getDeepLevel() { + return deepLevel; + } + + /** + * This method should be used for method + * of binary expansion + * Returned table will contains parents, located at + * 1, 2, 4, 8... levels upper + * + * @return table of parents + */ + @NotNull + public String[] getParentsTable() { + return parentsTable; + } +} diff --git a/src/main/resources/logging.properties b/src/main/resources/logging.properties new file mode 100644 index 0000000..a519949 --- /dev/null +++ b/src/main/resources/logging.properties @@ -0,0 +1,6 @@ +handlers= java.util.logging.FileHandler + +java.util.logging.FileHandler.pattern = .liteVCS\\logs\\application_log.txt +java.util.logging.FileHandler.limit = 1000000 +java.util.logging.FileHandler.count = 5 +java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter \ No newline at end of file diff --git a/src/test/java/ru/spbau/lobanov/liteVCS/logic/AlgorithmsTest.java b/src/test/java/ru/spbau/lobanov/liteVCS/logic/AlgorithmsTest.java new file mode 100644 index 0000000..e89cbdb --- /dev/null +++ b/src/test/java/ru/spbau/lobanov/liteVCS/logic/AlgorithmsTest.java @@ -0,0 +1,128 @@ +package ru.spbau.lobanov.liteVCS.logic; + +import org.jetbrains.annotations.NotNull; +import org.junit.Test; +import ru.spbau.lobanov.liteVCS.primitives.VersionNode; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import static org.junit.Assert.*; +import static ru.spbau.lobanov.liteVCS.logic.Algorithms.*; + +public class AlgorithmsTest { + // its longer then length of max jump in binary expansion + private static final PathManager pathExample = new PathManager(100_000); + + // just usual tree + private static final FakeManager treeExample = new FakeManager(); + + static { + try { + treeExample.addVersionNode((String) null); // 0 + treeExample.addVersionNode("0"); // 1 + treeExample.addVersionNode("0"); // 2 + treeExample.addVersionNode("2"); // 3 + treeExample.addVersionNode("3"); // 4 + treeExample.addVersionNode("3"); // 5 + treeExample.addVersionNode("4"); // 6 + treeExample.addVersionNode("1"); // 7 + treeExample.addVersionNode("7"); // 8 + treeExample.addVersionNode("1"); // 9 + treeExample.addVersionNode("2"); // 10 + treeExample.addVersionNode("5"); // 11 + treeExample.addVersionNode("4"); // 12 + treeExample.addVersionNode("6"); // 13 + treeExample.addVersionNode("8"); // 14 + treeExample.addVersionNode("4"); // 15 + } catch (Exception ignored) {} + } + + @Test + public void smallDataLCATest() throws Exception { + assertEquals("0", findLowestCommonAncestor("14", "13", treeExample)); + assertEquals("4", findLowestCommonAncestor("12", "13", treeExample)); + assertEquals("3", findLowestCommonAncestor("13", "3", treeExample)); + assertEquals("0", findLowestCommonAncestor("1", "15", treeExample)); + assertEquals("0", findLowestCommonAncestor("0", "15", treeExample)); + assertEquals("2", findLowestCommonAncestor("10", "11", treeExample)); + assertEquals("1", findLowestCommonAncestor("14", "9", treeExample)); + assertEquals("8", findLowestCommonAncestor("8", "8", treeExample)); + } + + @Test + public void bigDataLCATest() throws Exception { + assertEquals("0", findLowestCommonAncestor("0", "99999", pathExample)); + assertEquals("7", findLowestCommonAncestor("7", "99435", pathExample)); + assertEquals("34", findLowestCommonAncestor("34", "9934", pathExample)); + assertEquals("53", findLowestCommonAncestor("53", "99977", pathExample)); + } + + @Test + public void allParentsSmallDataTest() throws DataManager.LostFileException, DataManager.BrokenFileException { + List parents = getAllParents("13", 7, treeExample); + VersionNode[] answer = {treeExample.fetchVersionNode("13"), treeExample.fetchVersionNode("6"), + treeExample.fetchVersionNode("4"), treeExample.fetchVersionNode("3"), + treeExample.fetchVersionNode("2"), treeExample.fetchVersionNode("0")}; + assertEquals(answer.length, parents.size()); + for (int i = 0; i < answer.length; i++) { + assertSame(answer[i], parents.get(i)); + } + + List parents2 = getAllParents("14", 4, treeExample); + VersionNode[] answer2 = {treeExample.fetchVersionNode("14"), treeExample.fetchVersionNode("8"), + treeExample.fetchVersionNode("7"), treeExample.fetchVersionNode("1")}; + assertEquals(answer2.length, parents2.size()); + for (int i = 0; i < answer2.length; i++) { + assertSame(answer2[i], parents2.get(i)); + } + } + + private static class PathManager extends DataManager { + + private final ArrayList versionNodes = new ArrayList<>(); + + PathManager(int size) { + super(""); + try { + versionNodes.add(Algorithms.createRootNode("0", "")); + for (int i = 1; i < size; i++) { + versionNodes.add(Algorithms.createVersionNode("", (i - 1) + "", this)); + } + } catch (Exception ignored){} + } + + @NotNull + @Override + public VersionNode fetchVersionNode(@NotNull String id) { + int i = Integer.parseInt(id); + return versionNodes.get(i); + } + } + + private static class FakeManager extends DataManager { + private final HashMap map = new HashMap<>(); + + FakeManager() { + super(""); + } + + void addVersionNode(String parentID) throws BrokenFileException, LostFileException { + VersionNode versionNode; + if (parentID == null) { + versionNode = Algorithms.createRootNode("0", ""); + } else { + versionNode = Algorithms.createVersionNode("", parentID, this); + } + map.put(map.size() + "", versionNode); + } + + @NotNull + @Override + public VersionNode fetchVersionNode(@NotNull String id) { + return map.get(id); + } + + } +} \ No newline at end of file diff --git a/src/test/java/ru/spbau/lobanov/liteVCS/logic/ConsoleWorkerTest.java b/src/test/java/ru/spbau/lobanov/liteVCS/logic/ConsoleWorkerTest.java new file mode 100644 index 0000000..4d62592 --- /dev/null +++ b/src/test/java/ru/spbau/lobanov/liteVCS/logic/ConsoleWorkerTest.java @@ -0,0 +1,116 @@ +package ru.spbau.lobanov.liteVCS.logic; + +import org.junit.Test; +import ru.spbau.lobanov.liteVCS.ConsoleWorker; +import ru.spbau.lobanov.liteVCS.primitives.Header; +import ru.spbau.lobanov.liteVCS.primitives.Stage; + +import java.io.PrintStream; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ConsoleWorkerTest { + + @Test(expected = LiteVCS.UnknownBranchException.class) + public void unknownBranchTest() throws Exception { + System.setOut(mock(PrintStream.class)); + + Header header = mock(Header.class); + when(header.getCurrentBranchName()).thenReturn("master"); + Stage stage = mock(Stage.class); + when(stage.isEmpty()).thenReturn(true); + + DataManager dataManager = mock(DataManager.class); + when(dataManager.hasBranch("branch")).thenReturn(false); + when(dataManager.getHeader()).thenReturn(header); + when(dataManager.getStage()).thenReturn(stage); + + ConsoleWorker consoleWorker = new ConsoleWorker(new LiteVCS(dataManager)); + consoleWorker.execute("switch_branch", new String[]{"branch"}); + } + + @Test(expected = LiteVCS.UncommittedChangesException.class) + public void notEmptyStageTest() throws Exception { + System.setOut(mock(PrintStream.class)); + + Header header = mock(Header.class); + when(header.getCurrentBranchName()).thenReturn("master"); + Stage stage = mock(Stage.class); + when(stage.isEmpty()).thenReturn(false); + + DataManager dataManager = mock(DataManager.class); + when(dataManager.hasBranch("branch")).thenReturn(false); + when(dataManager.getHeader()).thenReturn(header); + when(dataManager.getStage()).thenReturn(stage); + + ConsoleWorker consoleWorker = new ConsoleWorker(new LiteVCS(dataManager)); + consoleWorker.execute("switch_branch", new String[]{"branch"}); + } + + @Test(expected = LiteVCS.SwitchOnCurrentBranchException.class) + public void switchOnCurrentBranchTest() throws Exception { + System.setOut(mock(PrintStream.class)); + + Header header = mock(Header.class); + when(header.getCurrentBranchName()).thenReturn("branch"); + Stage stage = mock(Stage.class); + when(stage.isEmpty()).thenReturn(true); + + DataManager dataManager = mock(DataManager.class); + when(dataManager.hasBranch("branch")).thenReturn(false); + when(dataManager.getHeader()).thenReturn(header); + when(dataManager.getStage()).thenReturn(stage); + + ConsoleWorker consoleWorker = new ConsoleWorker(new LiteVCS(dataManager)); + consoleWorker.execute("switch_branch", new String[]{"branch"}); + } + + @Test(expected = LiteVCS.RemoveActiveBranchException.class) + public void removeActiveBranchTest() throws Exception { + System.setOut(mock(PrintStream.class)); + + Header header = mock(Header.class); + when(header.getCurrentBranchName()).thenReturn("master"); + Stage stage = mock(Stage.class); + when(stage.isEmpty()).thenReturn(true); + + DataManager dataManager = mock(DataManager.class); + when(dataManager.hasBranch("master")).thenReturn(false); + when(dataManager.getHeader()).thenReturn(header); + when(dataManager.getStage()).thenReturn(stage); + + ConsoleWorker consoleWorker = new ConsoleWorker(new LiteVCS(dataManager)); + consoleWorker.execute("remove_branch", new String[]{"master"}); + } + + @Test(expected = LiteVCS.IllegalBranchToMergeException.class) + public void selfMergeTest() throws Exception { + System.setOut(mock(PrintStream.class)); + + Header header = mock(Header.class); + when(header.getCurrentBranchName()).thenReturn("master"); + + DataManager dataManager = mock(DataManager.class); + when(dataManager.hasBranch("master")).thenReturn(false); + when(dataManager.getHeader()).thenReturn(header); + + ConsoleWorker consoleWorker = new ConsoleWorker(new LiteVCS(dataManager)); + consoleWorker.execute("merge_branch", new String[]{"master", "commit"}); + } + + @Test(expected = LiteVCS.ConflictNameException.class) + public void cloneBranchTest() throws Exception { + System.setOut(mock(PrintStream.class)); + + Header header = mock(Header.class); + when(header.getCurrentBranchName()).thenReturn("master"); + + DataManager dataManager = mock(DataManager.class); + when(dataManager.hasBranch("branch")).thenReturn(true); + when(dataManager.getHeader()).thenReturn(header); + + ConsoleWorker consoleWorker = new ConsoleWorker(new LiteVCS(dataManager)); + consoleWorker.execute("create_branch", new String[]{"branch"}); + } +} diff --git a/src/test/java/ru/spbau/lobanov/liteVCS/logic/DataManagerTest.java b/src/test/java/ru/spbau/lobanov/liteVCS/logic/DataManagerTest.java new file mode 100644 index 0000000..b006c9a --- /dev/null +++ b/src/test/java/ru/spbau/lobanov/liteVCS/logic/DataManagerTest.java @@ -0,0 +1,192 @@ +package ru.spbau.lobanov.liteVCS.logic; + +import com.google.common.io.Files; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import ru.spbau.lobanov.liteVCS.primitives.*; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import static org.junit.Assert.*; + +public class DataManagerTest { + private static final String workspace = "test_workspace"; + + @Before + public void initRepository() throws Exception { + DataManager dataManager = new DataManager(workspace); + dataManager.initRepository(); + assertNotNull(dataManager.getHeader()); + assertNotNull(dataManager.getStage()); + File dir = Paths.get(workspace, ".liteVCS").toFile(); + File[] files = dir.listFiles(); + assertNotNull(files); + assertEquals(7, files.length); + } + + @After + public void clearWorkspace() throws DataManager.RepositoryNotInitializedException { + DataManager dataManager = new DataManager(workspace); + dataManager.clearWorkingCopy(); + dataManager.uninstallRepository(); + File[] files = new File(workspace).listFiles(); + assertNotNull(files); + assertEquals(0, files.length); + } + + @Test + public void addAndFetchVersionNode() throws Exception { + DataManager dataManager = new DataManager(workspace); + VersionNode versionNode = new VersionNode("id", 2, new String[0]); + String versionID = dataManager.addVersionNode(versionNode); + VersionNode copy = dataManager.fetchVersionNode(versionID); + assertEquals(versionNode.getCommitID(), copy.getCommitID()); + assertEquals(versionNode.getDeepLevel(), copy.getDeepLevel()); + } + + @Test + public void addAndFetchContentDescriptor() throws Exception { + DataManager dataManager = new DataManager(workspace); + ContentDescriptor descriptor = ContentDescriptor + .builder() + .addFile("1", "2") + .addFile("hello", "world") + .build(); + String descriptorID = dataManager.addContentDescriptor(descriptor); + ContentDescriptor copy = dataManager.fetchContentDescriptor(descriptorID); + assertEquals(descriptor.getFiles(), copy.getFiles()); + } + + @Test + public void addAndFetchCommit() throws Exception { + DataManager dataManager = new DataManager(workspace); + Commit commit = new Commit("id", "mes", 19980108, "a"); + String commitID = dataManager.addCommit(commit); + Commit commit1 = dataManager.fetchCommit(commitID); + assertEquals(commit.getContentDescriptorID(), commit1.getContentDescriptorID()); + assertEquals(commit.getAuthor(), commit1.getAuthor()); + assertEquals(commit.getCommitMessage(), commit1.getCommitMessage()); + } + + @Test + public void addAndLoadFile() throws Exception { + File file = Paths.get(workspace, "a.txt").toFile(); + Files.touch(file); + DataManager dataManager = new DataManager(workspace); + String id = dataManager.addFile("a.txt"); + dataManager.removeFile("a.txt"); + assertFalse(file.exists()); + dataManager.loadFile(id, "a.txt"); + assertTrue(file.exists()); + assertEquals(id, dataManager.hashFile("a.txt")); + } + + @Test + public void addAndFetchBranch() throws Exception { + DataManager dataManager = new DataManager(workspace); + Branch branch = new Branch("id", "mes"); + dataManager.addBranch(branch); + Branch branch1 = dataManager.fetchBranch(branch.getName()); + assertEquals(branch.getName(), branch1.getName()); + assertEquals(branch.getVersionNodeID(), branch1.getVersionNodeID()); + } + + @Test + public void hasAndRemoveBranch() throws Exception { + DataManager dataManager = new DataManager(workspace); + assertFalse(dataManager.hasBranch("branch")); + dataManager.addBranch(new Branch("d", "branch")); + assertTrue(dataManager.hasBranch("branch")); + dataManager.removeBranch("branch"); + assertFalse(dataManager.hasBranch("branch")); + } + + @Test + public void putAndGetHeader() throws Exception { + DataManager dataManager = new DataManager(workspace); + Header header = new Header("a", "branch"); + dataManager.putHeader(header); + Header header1 = dataManager.getHeader(); + assertEquals(header.getAuthor(), header1.getAuthor()); + assertEquals(header.getCurrentBranchName(), header1.getCurrentBranchName()); + } + + @Test + public void putAndGetStage() throws Exception { + DataManager dataManager = new DataManager(workspace); + Stage stage = Stage + .builder() + .addFile("1", "2") + .addFile("hello", "world") + .removeFile("3") + .build(); + dataManager.putStage(stage); + Stage stage1 = dataManager.getStage(); + assertEquals(stage.getChangedFiles(), stage1.getChangedFiles()); + assertEquals(stage.getRemovedFiles(), stage1.getRemovedFiles()); + } + + @Test + public void removeFile() throws Exception { + String relativePath = Paths.get("folder", "folder2", "a.txt").toString(); + Path path = Paths.get(workspace, relativePath); + + Files.createParentDirs(path.toFile()); + Files.touch(path.toFile()); + Files.touch(Paths.get(workspace, "folder", "b.txt").toFile()); + + DataManager dataManager = new DataManager(workspace); + dataManager.removeFile(relativePath); + assertFalse(path.toFile().exists()); + assertFalse(path.getParent().toFile().exists()); + assertTrue(path.getParent().getParent().toFile().exists()); + } + + @Test + public void workingCopy() throws Exception { + File file = Paths.get(workspace, "a.txt").toFile(); + Files.touch(file); + + file = Paths.get(workspace, "folder1", "a.txt").toFile(); + Files.createParentDirs(file); + Files.touch(file); + + file = Paths.get(workspace, "folder1", "folder2", "b.txt").toFile(); + Files.createParentDirs(file); + Files.touch(file); + + List wc = (new DataManager(workspace)).workingCopyFiles(); + wc.sort(String::compareTo); + + assertEquals(3, wc.size()); + assertTrue(wc.contains(Paths.get("a.txt").toString())); + assertTrue(wc.contains(Paths.get("folder1", "a.txt").toString())); + assertTrue(wc.contains(Paths.get("folder1", "folder2", "b.txt").toString())); + } + + @Test + public void hashFileTest() throws Exception { + PrintWriter out = new PrintWriter(Paths.get(workspace, "a.txt").toFile()); + out.println("hello"); + out.close(); + + out = new PrintWriter(Paths.get(workspace, "a2.txt").toFile()); + out.println("hello"); + out.close(); + + out = new PrintWriter(Paths.get(workspace, "b.txt").toFile()); + out.println("olleh"); + out.close(); + + DataManager dataManager = new DataManager(workspace); + assertEquals(dataManager.hashFile("a.txt"), dataManager.hashFile("a.txt")); + assertEquals(dataManager.hashFile("a.txt"), dataManager.hashFile("a2.txt")); + assertNotEquals(dataManager.hashFile("a2.txt"), dataManager.hashFile("b.txt")); + } +} \ No newline at end of file diff --git a/src/test/java/ru/spbau/lobanov/liteVCS/logic/LiteVCSGeneralTests.java b/src/test/java/ru/spbau/lobanov/liteVCS/logic/LiteVCSGeneralTests.java new file mode 100644 index 0000000..d92c00f --- /dev/null +++ b/src/test/java/ru/spbau/lobanov/liteVCS/logic/LiteVCSGeneralTests.java @@ -0,0 +1,72 @@ +package ru.spbau.lobanov.liteVCS.logic; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class LiteVCSGeneralTests { + + @Test + public void addBranchSwitchChangeSwitchMerge() throws Exception { + VirtualDataManager dataManager = new VirtualDataManager(); + LiteVCS liteVCS = new LiteVCS(dataManager); + liteVCS.init(); + dataManager.writeFile("a.txt", "versiona1"); + String initialState = dataManager.hashFile("a.txt"); + liteVCS.add("a.txt"); + liteVCS.commit("commit #1"); + liteVCS.createBranch("br"); + liteVCS.switchBranch("br"); + dataManager.writeFile("a.txt", "versiona2"); + String lastState = dataManager.hashFile("a.txt"); + liteVCS.add("a.txt"); + liteVCS.commit("commit2"); + liteVCS.switchBranch("master"); + assertEquals(initialState, dataManager.hashFile("a.txt")); + liteVCS.mergeBranch("br", "mesage"); + liteVCS.reset("a.txt"); + assertEquals(lastState, dataManager.hashFile("a.txt")); + } + + @Test + public void addBranchChangeSwitchChangeSwitchMerge() throws Exception { + VirtualDataManager dataManager = new VirtualDataManager(); + LiteVCS liteVCS = new LiteVCS(dataManager); + liteVCS.init(); + + dataManager.writeFile("a.txt", "versiona1"); + + dataManager.writeFile("b.txt", "versionb1"); + String initialStateB = dataManager.hashFile("b.txt"); + + liteVCS.add("a.txt"); + liteVCS.add("b.txt"); + liteVCS.commit("commit #1"); + liteVCS.createBranch("br"); + + + dataManager.writeFile("a.txt", "versiona2"); + String resultStateA = dataManager.hashFile("a.txt"); + liteVCS.add("a.txt"); + liteVCS.commit("commit #2"); + + liteVCS.switchBranch("br"); + + dataManager.writeFile("b.txt", "versionb2"); + String resultStateB = dataManager.hashFile("b.txt"); + liteVCS.add("b.txt"); + liteVCS.commit("commit3"); + + liteVCS.switchBranch("master"); + assertEquals(resultStateA, dataManager.hashFile("a.txt")); + assertEquals(initialStateB, dataManager.hashFile("b.txt")); + + liteVCS.mergeBranch("br", "mesage"); + liteVCS.reset("a.txt"); + liteVCS.reset("b.txt"); + + + assertEquals(resultStateA, dataManager.hashFile("a.txt")); + assertEquals(resultStateB, dataManager.hashFile("b.txt")); + } +} \ No newline at end of file diff --git a/src/test/java/ru/spbau/lobanov/liteVCS/logic/LiteVCSTest.java b/src/test/java/ru/spbau/lobanov/liteVCS/logic/LiteVCSTest.java new file mode 100644 index 0000000..4813f45 --- /dev/null +++ b/src/test/java/ru/spbau/lobanov/liteVCS/logic/LiteVCSTest.java @@ -0,0 +1,305 @@ +package ru.spbau.lobanov.liteVCS.logic; + +import org.junit.Test; +import ru.spbau.lobanov.liteVCS.logic.LiteVCS.FileStatus; +import ru.spbau.lobanov.liteVCS.logic.LiteVCS.StageStatus; +import ru.spbau.lobanov.liteVCS.primitives.*; + +import java.util.Map; + +import static org.junit.Assert.*; +import static ru.spbau.lobanov.liteVCS.logic.LiteVCS.FileStatus.*; +import static ru.spbau.lobanov.liteVCS.logic.LiteVCS.StageStatus.REMOVED; +import static ru.spbau.lobanov.liteVCS.logic.LiteVCS.StageStatus.UPDATED; + +public class LiteVCSTest { + @Test + public void hello() throws Exception { + VirtualDataManager dataManager = new VirtualDataManager(); + LiteVCS liteVCS = new LiteVCS(dataManager); + + liteVCS.init(); + liteVCS.hello("author"); + Header header = dataManager.getHeader(); + assertEquals("author", header.getAuthor()); + } + + @Test + public void add() throws Exception { + VirtualDataManager dataManager = new VirtualDataManager(); + LiteVCS liteVCS = new LiteVCS(dataManager); + + liteVCS.init(); + dataManager.writeFile("a.txt", "data"); + liteVCS.add("a.txt"); + Stage stage = dataManager.getStage(); + String id = stage.getChangedFiles().get("a.txt"); + assertNotNull(id); + VirtualFile f = (VirtualFile) dataManager.fetchFile(id); + assertEquals("data", f.getValue()); + } + + @Test + public void commit() throws Exception { + VirtualDataManager dataManager = new VirtualDataManager(); + LiteVCS liteVCS = new LiteVCS(dataManager); + + liteVCS.init(); + dataManager.writeFile("a.txt", "data"); + liteVCS.add("a.txt"); + liteVCS.commit("message"); + assertEquals(2, dataManager.commits.size()); + } + + @Test + public void logs() throws Exception { + VirtualDataManager dataManager = new VirtualDataManager(); + LiteVCS liteVCS = new LiteVCS(dataManager); + + liteVCS.init(); + dataManager.writeFile("a.txt", "data"); + liteVCS.add("a.txt"); + liteVCS.commit("message"); + dataManager.writeFile("b.txt", "data2"); + liteVCS.add("b.txt"); + liteVCS.commit("message2"); + + Commit commit1 = liteVCS.history("2").get(1); + Commit commit2 = liteVCS.history("2").get(0); + + assertEquals("message", commit1.getCommitMessage()); + assertEquals("message2", commit2.getCommitMessage()); + } + + @Test + public void createBranch() throws Exception { + VirtualDataManager dataManager = new VirtualDataManager(); + LiteVCS liteVCS = new LiteVCS(dataManager); + + liteVCS.init(); + dataManager.writeFile("a.txt", "data"); + liteVCS.add("a.txt"); + liteVCS.commit("message"); + + liteVCS.createBranch("branch"); + assertEquals(2, dataManager.branches.size()); + Branch master = dataManager.branches.get("master"); + Branch branch = dataManager.branches.get("branch"); + assertEquals(master.getVersionNodeID(), branch.getVersionNodeID()); + } + + @Test + public void removeBranch() throws Exception { + VirtualDataManager dataManager = new VirtualDataManager(); + LiteVCS liteVCS = new LiteVCS(dataManager); + + liteVCS.init(); + dataManager.writeFile("a.txt", "data"); + liteVCS.add("a.txt"); + liteVCS.commit("message"); + + liteVCS.createBranch("branch"); + assertEquals(2, dataManager.branches.size()); + liteVCS.removeBranch("branch"); + assertEquals(1, dataManager.branches.size()); + } + + @Test + public void mergeBranch() throws Exception { + VirtualDataManager dataManager = new VirtualDataManager(); + LiteVCS liteVCS = new LiteVCS(dataManager); + + liteVCS.init(); + + liteVCS.createBranch("branch"); + + dataManager.writeFile("a.txt", "data"); + liteVCS.add("a.txt"); + liteVCS.commit("message"); + + liteVCS.switchBranch("branch"); + dataManager.writeFile("b.txt", "data2"); + liteVCS.add("b.txt"); + liteVCS.commit("message2"); + + liteVCS.mergeBranch("master", "message3"); + + assertEquals(2, dataManager.branches.size()); + String versionNodeID = dataManager.branches.get("branch").getVersionNodeID(); + String commitID = dataManager.versions.get(versionNodeID).getCommitID(); + String descriptorID = dataManager.commits.get(commitID).getContentDescriptorID(); + ContentDescriptor descriptor = dataManager.descriptors.get(descriptorID); + assertEquals("data".hashCode(), dataManager.fetchFile(descriptor.getFiles().get("a.txt")).hashCode()); + assertEquals("data2".hashCode(), dataManager.fetchFile(descriptor.getFiles().get("b.txt")).hashCode()); + } + + @Test + public void switchBranch() throws Exception { + VirtualDataManager dataManager = new VirtualDataManager(); + LiteVCS liteVCS = new LiteVCS(dataManager); + + liteVCS.init(); + + liteVCS.createBranch("branch"); + liteVCS.switchBranch("branch"); + + dataManager.writeFile("a.txt", "data"); + liteVCS.add("a.txt"); + liteVCS.commit("message"); + + + assertEquals(1, dataManager.workingCopy.size()); + liteVCS.switchBranch("master"); + assertEquals(0, dataManager.workingCopy.size()); + } + +// @Test +// public void reset() throws Exception { +// VirtualDataManager dataManager = new VirtualDataManager(); +// LiteVCS liteVCS = new LiteVCS(dataManager); +// +// liteVCS.init(); +// +// dataManager.writeFile("a.txt", "data"); +// liteVCS.add("a.txt"); +// liteVCS.commit("message"); +// +// dataManager.writeFile("a.txt", "data2"); +// liteVCS.add("a.txt"); +// dataManager.writeFile("b.txt", "data"); +// +// liteVCS.reset(); +// +// assertEquals(1, dataManager.workingCopy.size()); +// assertEquals("data".hashCode() + "", dataManager.hash("a.txt")); +// } + + @Test + public void checkout() throws Exception { + VirtualDataManager dataManager = new VirtualDataManager(); + LiteVCS liteVCS = new LiteVCS(dataManager); + + liteVCS.init(); + + dataManager.writeFile("a.txt", "data"); + liteVCS.add("a.txt"); + liteVCS.commit("message"); + + String versionNodeID = dataManager.branches.get("master").getVersionNodeID(); + String commitID = dataManager.versions.get(versionNodeID).getCommitID(); + String descriptorID = dataManager.commits.get(commitID).getContentDescriptorID(); + + dataManager.writeFile("a.txt", "data2"); + liteVCS.add("a.txt"); + dataManager.writeFile("b.txt", "data"); + + liteVCS.checkout(descriptorID); + assertEquals(1, dataManager.workingCopy.size()); + assertEquals("data".hashCode() + "", dataManager.hashFile("a.txt")); + } + + @Test + public void clean() throws Exception { + VirtualDataManager dataManager = new VirtualDataManager(); + LiteVCS liteVCS = new LiteVCS(dataManager); + + liteVCS.init(); + + dataManager.writeFile("a.txt", "data"); + liteVCS.add("a.txt"); + liteVCS.commit("message"); + + dataManager.writeFile("b.txt", "data2"); + liteVCS.add("b.txt"); + dataManager.writeFile("b.txt", "data3"); + + dataManager.writeFile("c.txt", "data3"); + + liteVCS.clean(); + assertEquals(2, dataManager.workingCopy.size()); + assertTrue(dataManager.workingCopy.containsKey("a.txt")); + assertTrue(dataManager.workingCopy.containsKey("b.txt")); + assertEquals("data3".hashCode(), dataManager.workingCopy.get("b.txt").hashCode()); + } + + @Test + public void remove() throws Exception { + VirtualDataManager dataManager = new VirtualDataManager(); + LiteVCS liteVCS = new LiteVCS(dataManager); + + liteVCS.init(); + + dataManager.writeFile("a.txt", "data"); + liteVCS.add("a.txt"); + liteVCS.commit("message"); + + liteVCS.remove("a.txt"); + assertFalse(dataManager.workingCopy.containsKey("a.txt")); + liteVCS.reset("a.txt"); + assertTrue(dataManager.workingCopy.containsKey("a.txt")); + + liteVCS.remove("a.txt"); + liteVCS.commit("1"); + assertFalse(dataManager.workingCopy.containsKey("a.txt")); + String descriptionID = liteVCS.history("1").get(0).getContentDescriptorID(); + liteVCS.checkout(descriptionID); + assertFalse(dataManager.workingCopy.containsKey("a.txt")); + } + + @Test + public void reset() throws Exception { + VirtualDataManager dataManager = new VirtualDataManager(); + LiteVCS liteVCS = new LiteVCS(dataManager); + + liteVCS.init(); + + dataManager.writeFile("a.txt", "data"); + liteVCS.add("a.txt"); + liteVCS.commit("message"); + + dataManager.writeFile("a.txt", "data2"); + liteVCS.add("a.txt"); + + liteVCS.reset("a.txt"); + assertEquals("data".hashCode(), dataManager.workingCopy.get("a.txt").hashCode()); + assertFalse(dataManager.getStage().getChangedFiles().containsKey("a.txt")); + } + + @Test + public void workingCopyStatus() throws Exception { + VirtualDataManager dataManager = new VirtualDataManager(); + LiteVCS liteVCS = new LiteVCS(dataManager); + liteVCS.init(); + + dataManager.writeFile("a.txt", "data"); + Map result = liteVCS.workingCopyStatus(); + assertEquals(UNKNOWN, result.get("a.txt")); + + liteVCS.add("a.txt"); + result = liteVCS.workingCopyStatus(); + assertEquals(NOT_CHANGED, result.get("a.txt")); + + dataManager.writeFile("a.txt", "data2"); + result = liteVCS.workingCopyStatus(); + assertEquals(CHANGED, result.get("a.txt")); + + dataManager.removeFile("a.txt"); + result = liteVCS.workingCopyStatus(); + assertEquals(DISAPPEARED, result.get("a.txt")); + } + + @Test + public void stageStatus() throws Exception { + VirtualDataManager dataManager = new VirtualDataManager(); + LiteVCS liteVCS = new LiteVCS(dataManager); + liteVCS.init(); + + dataManager.writeFile("a.txt", "data"); + liteVCS.add("a.txt"); + Map result = liteVCS.stageStatus(); + + liteVCS.remove("a.txt"); + result = liteVCS.stageStatus(); + assertEquals(REMOVED, result.get("a.txt")); + } +} \ No newline at end of file diff --git a/src/test/java/ru/spbau/lobanov/liteVCS/logic/VirtualDataManager.java b/src/test/java/ru/spbau/lobanov/liteVCS/logic/VirtualDataManager.java new file mode 100644 index 0000000..39f3739 --- /dev/null +++ b/src/test/java/ru/spbau/lobanov/liteVCS/logic/VirtualDataManager.java @@ -0,0 +1,187 @@ +package ru.spbau.lobanov.liteVCS.logic; + +import org.jetbrains.annotations.NotNull; +import ru.spbau.lobanov.liteVCS.primitives.*; + +import java.io.File; +import java.io.IOException; +import java.util.*; + +public class VirtualDataManager extends DataManager { + + private static final String ROOT_VERSION_NODE_ID = "root"; + + boolean isInitialized; + HashMap branches = new HashMap<>(); + HashMap commits = new HashMap<>(); + HashMap descriptors = new HashMap<>(); + HashMap versions = new HashMap<>(); + HashMap files = new HashMap<>(); + Header header; + Stage stage; + HashMap workingCopy = new HashMap<>(); + + + VirtualDataManager() { + super(""); + } + + private static String createRandomKey(Set used) { + String key; + do { + key = "khfd" + Math.random() + "efd"; + } while (used.contains(key)); + return key; + } + + public static String hash(File file) { + return "" + file.hashCode(); + } + + void initRepository() throws RecreatingRepositoryException { + if (isInitialized) { + throw new RecreatingRepositoryException(""); + } + isInitialized = true; + try { + String initialDescriptorID = addContentDescriptor(ContentDescriptor.EMPTY); + String initialCommitID = addCommit(new Commit(initialDescriptorID, "Initial commit", + System.currentTimeMillis(), "lVCS")); + VersionNode start = Algorithms.createRootNode(ROOT_VERSION_NODE_ID, initialCommitID); + versions.put(ROOT_VERSION_NODE_ID, start); + Branch master = new Branch(ROOT_VERSION_NODE_ID, "master"); + addBranch(master); + Header header = new Header("Unknown", master.getName()); + putHeader(header); + putStage(Stage.EMPTY); + } catch (RepositoryNotInitializedException e) { + throw new Error(""); + } + } + + @NotNull + VersionNode fetchVersionNode(@NotNull String id) throws LostFileException, BrokenFileException { + return versions.get(id); + } + + @NotNull + String addVersionNode(@NotNull VersionNode versionNode) throws RepositoryNotInitializedException { + String id = createRandomKey(versions.keySet()); + versions.put(id, versionNode); + return id; + } + + @NotNull + ContentDescriptor fetchContentDescriptor(@NotNull String id) throws LostFileException, BrokenFileException { + return descriptors.get(id); + } + + @NotNull + String addContentDescriptor(@NotNull ContentDescriptor contentDescriptor) throws RepositoryNotInitializedException { + String id = createRandomKey(descriptors.keySet()); + descriptors.put(id, contentDescriptor); + return id; + } + + @NotNull + Commit fetchCommit(@NotNull String id) throws LostFileException, BrokenFileException { + return commits.get(id); + } + + @NotNull + String addCommit(@NotNull Commit commit) throws RepositoryNotInitializedException { + String id = createRandomKey(commits.keySet()); + commits.put(id, commit); + return id; + } + + @NotNull + File fetchFile(@NotNull String id) throws LostFileException { + return files.get(id); + } + + @NotNull + String addFile(@NotNull String path) throws RepositoryNotInitializedException { + VirtualFile file = (VirtualFile) workingCopy.get(path); + String id = "" + file.hashCode(); + files.put(id, file); + return id; + } + + void removeFile(String path) { + workingCopy.remove(path); + } + + void addBranch(@NotNull Branch branch) throws RepositoryNotInitializedException { + branches.put(branch.getName(), branch); + } + + @NotNull + Branch fetchBranch(@NotNull String name) throws LostFileException, BrokenFileException { + return branches.get(name); + } + + boolean hasBranch(@NotNull String name) { + return branches.containsKey(name); + } + + void removeBranch(@NotNull String name) throws LostFileException { + branches.remove(name); + } + @NotNull + Header getHeader() throws LostFileException, BrokenFileException { + return header; + } + + void putHeader(@NotNull Header header) throws RepositoryNotInitializedException { + this.header = header; + } + + @NotNull + Stage getStage() throws LostFileException, BrokenFileException { + return stage; + } + + void putStage(@NotNull Stage stage) throws RepositoryNotInitializedException { + this.stage = stage; + } + + void loadFile(@NotNull String fileID, @NotNull String targetPath) throws LostFileException, IOException { + workingCopy.put(targetPath, fetchFile(fileID)); + } + + List workingCopyFiles() { + return new ArrayList<>(workingCopy.keySet()); + } + + void clearWorkingCopy() { + workingCopy.clear(); + } + + /** + * Method allow to clear work space be removing all files including ROOT_DIRECTORY + * + * @throws RepositoryNotInitializedException if there was no repository + */ + void uninstallRepository() throws RepositoryNotInitializedException { + workingCopy.clear(); + branches.clear(); + files.clear(); + versions.clear(); + commits.clear(); + descriptors.clear(); + header = null; + stage = null; + isInitialized = false; + } + + void writeFile(String filename, String value) { + VirtualFile f = new VirtualFile(value); + workingCopy.put(filename, f); + } + + String hashFile(String filename) { + VirtualFile file = (VirtualFile) workingCopy.get(filename); + return "" + file.hashCode(); + } +} diff --git a/src/test/java/ru/spbau/lobanov/liteVCS/logic/VirtualFile.java b/src/test/java/ru/spbau/lobanov/liteVCS/logic/VirtualFile.java new file mode 100644 index 0000000..9a37153 --- /dev/null +++ b/src/test/java/ru/spbau/lobanov/liteVCS/logic/VirtualFile.java @@ -0,0 +1,42 @@ +package ru.spbau.lobanov.liteVCS.logic; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; + +public class VirtualFile extends File { + + private String value; + + public VirtualFile(@NotNull String value) { + super(""); + this.value = value; + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj.getClass() != VirtualFile.class) + return false; + VirtualFile vf = (VirtualFile) obj; + return value.equals(vf.value); + } + + @Override + public boolean exists() { + return true; + } + + @Override + public boolean isFile() { + return true; + } + + public String getValue() { + return value; + } +}