diff --git a/.travis.yml b/.travis.yml index f958b80..8e2ebb0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ jdk: os: - linux env: - - SOURCE_DIR_1=001-Lazy + - SOURCE_DIR_1=001-Lazy SOURCE_DIR_2=myVCS script: - - cd $SOURCE_DIR_1 && gradle check - + - cd $SOURCE_DIR_1 && gradle check && cd .. + - cd $SOURCE_DIR_2 && gradle check && cd .. diff --git a/myVCS/build.gradle b/myVCS/build.gradle new file mode 100644 index 0000000..a5f9264 --- /dev/null +++ b/myVCS/build.gradle @@ -0,0 +1,21 @@ +version '1.0-SNAPSHOT' + +apply plugin: 'java' +apply plugin: 'application' + +mainClassName = "com.maxim.Main" + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.11' + compile group: 'com.google.guava', name: 'guava', version: '19.0' + compile group: 'commons-codec', name: 'commons-codec', version: '1.5' + compile group: 'org.mockito', name: 'mockito-all', version: '1.8.4' + compile 'com.intellij:annotations:+@jar' + compile group: 'org.hamcrest', name: 'hamcrest-all', version: '1.3' +} diff --git a/myVCS/gradle/wrapper/gradle-wrapper.jar b/myVCS/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..6ffa237 Binary files /dev/null and b/myVCS/gradle/wrapper/gradle-wrapper.jar differ diff --git a/myVCS/gradle/wrapper/gradle-wrapper.properties b/myVCS/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bcf650f --- /dev/null +++ b/myVCS/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Mar 23 06:06:06 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/myVCS/gradlew b/myVCS/gradlew new file mode 100755 index 0000000..9aa616c --- /dev/null +++ b/myVCS/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/myVCS/gradlew.bat b/myVCS/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/myVCS/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/myVCS/settings.gradle b/myVCS/settings.gradle new file mode 100644 index 0000000..6a1d802 --- /dev/null +++ b/myVCS/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'myVCS' + diff --git a/myVCS/src/main/java/com/maxim/Main.java b/myVCS/src/main/java/com/maxim/Main.java new file mode 100644 index 0000000..06f70bf --- /dev/null +++ b/myVCS/src/main/java/com/maxim/Main.java @@ -0,0 +1,176 @@ +package com.maxim; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.maxim.vcs_impl.Vcs; +import com.maxim.vcs_impl.VcsImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Implements console mode + */ +public class Main { + private final Vcs vcs; + { + Vcs vcs1 = null; + try { + vcs1 = new VcsImpl(Paths.get(".")); + } catch (IOException e) { + e.printStackTrace(); + System.out.println("failed to init vcs"); + System.exit(3); + } + this.vcs = vcs1; + } + + private final String[] args; + + private final List commands = ImmutableList.of( + new Command(1, "add", "") { + @Override + public void execute(@NotNull String[] args) throws IOException { + Path path = Paths.get(parseArgs(args).get(0)); + vcs.add(path); + } + }, + + new Command(1, "commit", "message") { + @Override + public void execute(@NotNull String[] args) throws IOException { + vcs.commit(parseArgs(args).get(0)); + } + }, + new Command(1, "create-branch", "") { + @Override + public void execute(@NotNull String[] args) throws IOException { + vcs.createBranch(parseArgs(args).get(0)); + } + }, + new Command(2,"checkout", "-b", "") { + @Override + public void execute(@NotNull String[] args) throws IOException { + vcs.checkoutBranch(parseArgs(args).get(0)); + } + }, + new Command(2, "checkout", "-c", "") { + @Override + public void execute(@NotNull String[] args) throws IOException { + long arg = Long.parseLong(parseArgs(args).get(0)); + vcs.checkoutCommit(arg); + } + }, + new Command(1,"merge", "") { + @Override + public void execute(@NotNull String[] args) throws IOException { + vcs.merge(parseArgs(args).get(0)); + } + }, + new Command(2, "log", "-b") { + @Override + public void execute(@NotNull String[] args) throws IOException { + System.out.println("---- current branch:"); + System.out.println(vcs.getCurrentBranchName()); + System.out.println(); + System.out.println("---- all branches:"); + + vcs.logBranches().forEach(System.out::println); + } + }, + new Command(2, "log", "-c") { + @Override + public void execute(@NotNull String[] args) throws IOException { + System.out.println("---- current commit:"); + System.out.println(vcs.getCurrentCommitId()); + System.out.println(); + System.out.println("---- all commits:"); + + vcs.logCommits().forEach(System.out::println); + } + }, + new Command(1, "clean") { + @Override + public void execute(@NotNull String[] args) throws IOException { + vcs.clean(); + } + }, + new Command(1, "status") { + @Override + public void execute(@NotNull String[] args) throws IOException { + vcs.status() + .forEach((key, value) -> System.out.println(key + " " + value)); + } + }, + new Command(1, "rm", "") { + @Override + public void execute(@NotNull String[] args) throws IOException { + Path path = Paths.get(parseArgs(args).get(0)); + vcs.rm(path); + } + }, + new Command(1, "reset", "") { + @Override + public void execute(@NotNull String[] args) throws IOException { + Path path = Paths.get(parseArgs(args).get(0)); + vcs.reset(path); + } + } + ); + + public Main(String args[]) { + this.args = args; + } + + public static void main(String[] args) { + new Main(args).run(); + } + + public void run() { + List ok_commands = commands.stream().filter(command -> command.check(args)).collect(Collectors.toList()); + if (ok_commands.size() == 1) { + try { + Command command = ok_commands.get(0); + command.execute(args); + } catch (IOException e){ + e.printStackTrace(); + } + } else { + System.out.println("usage: \n"); + String help_message = commands.stream().map(Command::getMessage).collect(Collectors.joining("\n")); + System.out.println(help_message); + } + } + + private abstract static class Command { + private final ImmutableList args_names; + private final int prefix_len; + + public Command(int args_count, @NotNull String... arg_names) { + this.args_names = ImmutableList.copyOf(arg_names); + this.prefix_len = args_count; + } + + public String getMessage() { + return args_names.stream().collect(Collectors.joining(" ")); + } + + public boolean check(@NotNull String[] args) { + return args_names.size() == args.length && + ImmutableList.copyOf(args).subList(0, prefix_len).equals(args_names.subList(0, prefix_len)); + } + + @NotNull + public List parseArgs(@NotNull String[] args) { + return check(args) ? ImmutableList.copyOf(args).subList(prefix_len, args_names.size()) : new ArrayList<>(); + } + + public abstract void execute(String[] args) throws IOException; + } +} diff --git a/myVCS/src/main/java/com/maxim/vcs_impl/FileUtil.java b/myVCS/src/main/java/com/maxim/vcs_impl/FileUtil.java new file mode 100644 index 0000000..35f1d34 --- /dev/null +++ b/myVCS/src/main/java/com/maxim/vcs_impl/FileUtil.java @@ -0,0 +1,89 @@ +package com.maxim.vcs_impl; + +import com.maxim.vcs_objects.*; +import org.jetbrains.annotations.NotNull; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Class contains static methods to work with files + */ +public class FileUtil { + public static final String vcs_dir = ".vcs"; + public static final String blobs_dir = "blobs"; + public static final String commits_dir = "commits"; + public static final String branches_dir = "branches"; + public static final String index_file = "index"; + + /** + * creates VcsBlob from file + * throws RuntimeException + */ + @NotNull + public static VcsBlob getBlob(@NotNull Path path) { + try { + if (!Files.isRegularFile(path)) { + throw new IOException("not such file"); + } + return new VcsBlob(Files.readAllBytes(path)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * creates VcsBlobLink from file + * throws RuntimeException + */ + @NotNull + public static VcsBlobLink addBlob(@NotNull Path path, @NotNull Path blobs_dir) { + try { + VcsBlob blob = getBlob(path); + Path blob_path = Paths.get(blobs_dir + "", blob.md5_hash); + if (!Files.exists(blob_path)) { + Files.createFile(blob_path); + Files.write(blob_path, blob.bytes); + } + return new VcsBlobLink(blob.md5_hash); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + /** + * serializes object to path + */ + public static void writeObject(@NotNull Object object, @NotNull Path path) throws IOException { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + ObjectOutput out = new ObjectOutputStream(bos); + out.writeObject(object); + if (!Files.exists(path)) { + Files.createFile(path); + } else { + Files.delete(path); + Files.createFile(path); + } + Files.write(path, bos.toByteArray()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * deserialize object from path + */ + @NotNull + public static Object readObject(@NotNull Path path) throws IOException { + try (FileInputStream fin = new FileInputStream(path.toString())) { + ObjectInput ois = new ObjectInputStream(fin); + return ois.readObject(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + throw new RuntimeException(); + } +} diff --git a/myVCS/src/main/java/com/maxim/vcs_impl/Vcs.java b/myVCS/src/main/java/com/maxim/vcs_impl/Vcs.java new file mode 100644 index 0000000..e104f13 --- /dev/null +++ b/myVCS/src/main/java/com/maxim/vcs_impl/Vcs.java @@ -0,0 +1,114 @@ +package com.maxim.vcs_impl; + +import com.maxim.vcs_objects.VcsCommit; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +/** + * Interface for an implementation of version control system. + */ + +public interface Vcs { + /** + * if current branch doesn't exists + * throws an exception + * otherwise + * commits all tracking files, + * moves current branch head to new commit + * + * returns new commit + */ + + @NotNull + VcsCommit commit(@NotNull String message) throws IOException; + + /** + * if such commit exists + * checkouts commit with id=commit + * otherwise + * throws an Exception + */ + + void checkoutCommit(long commit_id) throws IOException; + + /** + * if such branch exists + * checkouts commit which is head of branch with name=branch_name + * otherwise + * throws an Exception + */ + + void checkoutBranch(@NotNull String branch_name) throws IOException; + + /** + * puts file if absent in list tracking files + */ + + void add(@NotNull Path path) throws IOException; + + /** + * returns list, which contains all commits + */ + + @NotNull + List logCommits() throws IOException; + + /** + * returns list, which contains all branches names + */ + + @NotNull + List logBranches() throws IOException; + + /** + * merges current branch with other_branch, deletes other_branch + */ + + VcsCommit merge(@NotNull String other_branch) throws IOException; + + /** + * creates a new branch with name other_branch_name and checkouts that branch + * new branch points to current commit + */ + + void createBranch(@NotNull String other_branch_name) throws IOException; + + /** + * resets file to previous version, if file staged + * otherwise throws an exception + */ + + void reset(@NotNull Path path) throws IOException; + + /** + * shows status: (path to file, "untracked" | "added" | "removed" | "committed" | "modified" + */ + @NotNull + Map status() throws IOException; + + /** + * removes file from path, also removes file from working copy + */ + void rm(@NotNull Path path) throws IOException; + + /** + * removes all untracked files + */ + + void clean() throws IOException; + + /** + * returns name of current branch, if it exists, otherwise "null" + */ + String getCurrentBranchName(); + + /** + * returns id of current commit, + * note: id of initial commit is null + */ + long getCurrentCommitId(); +} \ No newline at end of file diff --git a/myVCS/src/main/java/com/maxim/vcs_impl/VcsImpl.java b/myVCS/src/main/java/com/maxim/vcs_impl/VcsImpl.java new file mode 100644 index 0000000..cbdc883 --- /dev/null +++ b/myVCS/src/main/java/com/maxim/vcs_impl/VcsImpl.java @@ -0,0 +1,406 @@ +package com.maxim.vcs_impl; + +import com.google.common.collect.*; +import com.maxim.vcs_objects.VcsBlobLink; +import com.maxim.vcs_objects.VcsBranch; +import com.maxim.vcs_objects.VcsCommit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.Serializable; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +import static com.maxim.vcs_impl.FileUtil.*; +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + + +/** + * Implements methods of Interface VCS + */ +public class VcsImpl implements Vcs { + @NotNull + private Index index = new Index(VcsCommit.nullCommit.id); + @NotNull + private final Path root_path; + + private final Path blobs_dir_path; + private final Path commits_dir_path; + private final Path branches_dir_path; + private final Path index_file_path; + private final Path vcs_dir_path; + + + public VcsImpl(@NotNull Path root_path) throws IOException { + this.root_path = root_path.normalize(); + + blobs_dir_path = Paths.get(root_path.toString(), vcs_dir, blobs_dir); + commits_dir_path = Paths.get(root_path.toString(), vcs_dir, commits_dir); + branches_dir_path = Paths.get(root_path.toString(), vcs_dir, branches_dir); + index_file_path = Paths.get(root_path.toString(), vcs_dir, index_file); + vcs_dir_path = Paths.get(root_path.toString(), vcs_dir); + + Files.createDirectories(commits_dir_path); + Files.createDirectories(blobs_dir_path); + Files.createDirectories(branches_dir_path); + Files.createDirectories(branches_dir_path); + + writeCommit(VcsCommit.nullCommit); + + if (!Files.exists(index_file_path)) { + writeObject(index, index_file_path); + } + } + + @NotNull + @Override + public VcsCommit commit(@NotNull String message) throws IOException { + readIndex(); + + if (index.branch == null) { + throw new IOException("Please, checkout branch or create branch"); + } + + Map oldFiles = getCommittedFiles(); + List parents_ids = Collections.singletonList(index.commit_id); + + Map currentFiles = new TreeMap<>(loadFiles()); + oldFiles.forEach(currentFiles::putIfAbsent); + + VcsCommit new_commit = new VcsCommit(message, parents_ids, currentFiles); + writeCommit(new_commit); + + index.branch = index.branch.changeCommit(new_commit.id); + index = new Index(index.branch); + + writeBranch(index.branch); + writeIndex(); + return new_commit; + } + + @Override + public void checkoutCommit(long commit_id) throws IOException { + readIndex(); + + Map committedFiles = getCommittedFiles(); + for (String file_path : committedFiles.keySet()) { + try { + Files.delete(Paths.get(file_path)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + VcsCommit commit = readCommit(commit_id); + for (Map.Entry entry : commit.files.entrySet()) { + Path path = Paths.get(entry.getKey()); + Files.createDirectories(path.getParent()); + if (!Files.exists(path)) { + Files.createFile(path); + } + String blob_name = entry.getValue().md5_hash; + Path blob_path = Paths.get(blobs_dir_path.toString(), blob_name); + Files.copy(blob_path, path, REPLACE_EXISTING); + } + + index = new Index(commit_id); + + writeIndex(); + } + + @Override + public void checkoutBranch(@NotNull String branch_name) throws IOException { + readIndex(); + + VcsBranch branch = readBranch(branch_name); + checkoutCommit(branch.commit_id); + writeBranch(branch); + index = new Index(branch); + + writeIndex(); + } + + @Override + public void add(@NotNull Path path) throws IOException { + readIndex(); + + if (!Files.isRegularFile(path)){ + throw new IOException(path + " isn't regular file"); + } + if (!getTrackingNames().contains(path.toString())) { + index.added.add(path.toString()); + } + writeIndex(); + } + + @Override + public VcsCommit merge(@NotNull String other_branch_name) throws IOException { + readIndex(); + + if (index.branch == null) { + throw new IOException("Please, checkout branch or create branch"); + } + + VcsBranch other_branch = readBranch(other_branch_name); + VcsCommit other_commit = readCommit(other_branch.commit_id); + Map cur_files = getCommittedFiles(); + + MapDifference map_difference = Maps.difference(cur_files, other_commit.files); + if (map_difference.entriesDiffering().size() > 0) { + throw new IOException("files are differ:" + map_difference.entriesDiffering()); + } + + Map res_files = new TreeMap<>(); + cur_files.forEach(res_files::putIfAbsent); + other_commit.files.forEach(res_files::putIfAbsent); + + List parents_ids = ImmutableList.of(index.commit_id, other_commit.id); + VcsCommit new_commit = new VcsCommit("merged " + other_branch_name, parents_ids, res_files); + writeCommit(new_commit); + + VcsBranch branch = index.branch.changeCommit(new_commit.id); + writeBranch(branch); + + index = new Index(branch); + + if (!branch.name.equals(other_branch_name)) { + deleteBranch(other_branch_name); + } + + writeIndex(); + + return new_commit; + } + + @Override + public void createBranch(@NotNull String branch_name) throws IOException { + readIndex(); + + Path path = Paths.get(branches_dir_path + "", branch_name); + if (Files.exists(path)) { + throw new IOException("branch exits"); + } + VcsBranch branch = new VcsBranch(branch_name, index.commit_id); + writeBranch(branch); + checkoutBranch(branch_name); + + writeIndex(); + } + + @NotNull + @Override + public List logCommits() throws IOException { + try { + return Files.walk(commits_dir_path) + .filter(Files::isRegularFile) + .map(path -> { + try { + return (VcsCommit) readObject(path); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .sorted((commit1, commit2) -> { + if (commit1.date < commit2.date) { + return 0; + } + return commit1.date < commit2.date ? 1 : -1; + }) + .collect(Collectors.toList()); + } catch (RuntimeException e) { + throw (IOException) e.getCause(); + } + } + + @NotNull + @Override + public List logBranches() throws IOException { + return Files.walk(branches_dir_path) + .filter(Files::isRegularFile) + .map(Path::getFileName) + .map(Path::toString) + .collect(Collectors.toList()); + } + + @Override + public void clean() throws IOException { + Set tracking = getTrackingNames(); + Files.walk(root_path) + .filter(Files::isRegularFile) + .filter(entry -> !entry.startsWith(vcs_dir_path)) + .filter(entry -> !tracking.contains(entry.toString())) + .forEach(entry -> { + try { + Files.delete(entry); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + @Override + public void reset(@NotNull Path path) throws IOException { + Map committed = getCommittedFiles(); + if (!committed.containsKey(path.toString())) { + throw new IOException("no such committed file"); + } + String blob_name = committed.get(path.toString()).md5_hash; + Path blob_path = Paths.get(blobs_dir_path.toString(), blob_name); + Files.copy(blob_path, path, REPLACE_EXISTING); + } + + @NotNull + @Override + public Map status() throws IOException { + readIndex(); + + Map committedFiles = getCommittedFiles(); + Set committedNames = committedFiles.keySet(); + ImmutableMap.Builder mapBuilder = ImmutableMap.builder(); + Files.walk(root_path) + .filter(Files::isRegularFile) + .filter(entry -> !entry.startsWith(vcs_dir_path)) + .forEach(entry -> { + String state = "untracked"; + if (index.added.contains(entry.toString())) { + state = "added"; + } else if (index.removed.contains(entry.toString())) { + state = "removed"; + } else if (committedNames.contains(entry.toString())) { + String fileHash = committedFiles.get(entry.toString()).md5_hash; + if (getBlob(entry).md5_hash.equals(fileHash)) { + state = "committed"; + } else { + state = "modified"; + } + } + mapBuilder.put(entry, state); + }); + index.removed.forEach(entry -> mapBuilder.put(Paths.get(entry), "removed")); + return mapBuilder.build(); + } + + @Override + public void rm(@NotNull Path path) throws IOException { + readIndex(); + + if (!Files.exists(path) || !Files.isRegularFile(path)) { + throw new IOException("no such regular file " + path); + } + Files.delete(path); + + if (getTrackingNames().contains(path.toString())) { + if (index.added.contains(path.toString())) { + index.added.remove(path.toString()); + } else { + index.removed.add(path.toString()); + } + } + + writeIndex(); + } + + @Override + public String getCurrentBranchName() { + return index.branch == null ? "null" : index.branch.name; + } + + @Override + public long getCurrentCommitId() { + return index.commit_id; + } + + private void writeIndex() throws IOException { + if (index.branch != null) { + index.commit_id = index.branch.commit_id; + } + writeObject(index, index_file_path); + } + + private void readIndex() throws IOException { + index = (Index) readObject(index_file_path); + } + + private void writeCommit(@NotNull VcsCommit commit) throws IOException { + Path path = Paths.get(commits_dir_path + "", commit.id + ""); + writeObject(commit, path); + } + + private VcsCommit readCommit(long commit_id) throws IOException { + Path path = Paths.get(commits_dir_path + "", commit_id + ""); + return (VcsCommit) FileUtil.readObject(path); + } + + private void deleteBranch(@NotNull String branch_name) throws IOException { + readIndex(); + + Path branch_path = Paths.get(branches_dir_path + "", branch_name); + if (Files.exists(branch_path)) { + Files.delete(branch_path); + } + + writeIndex(); + } + + @NotNull + private Map loadFiles() throws IOException { + Set paths = getTrackingNames(); + try { + return Files.walk(root_path) + .filter(Files::isRegularFile) + .filter(entry -> paths.contains(entry.toString())) + .collect(Collectors.toMap( + Path::toString, + path1 -> FileUtil.addBlob(path1, blobs_dir_path))); + } catch (RuntimeException e) { + throw (IOException) e.getCause(); + } + } + + private void writeBranch(VcsBranch branch) throws IOException { + Path branch_path = Paths.get(branches_dir_path.toString(), branch.name); + writeObject(branch, branch_path); + } + + private VcsBranch readBranch(@NotNull String branch_name) throws IOException { + Path branch_path = Paths.get(branches_dir_path.toString(), branch_name); + return (VcsBranch) readObject(branch_path); + } + + @NotNull + private Set getTrackingNames() throws IOException { + Set added = index.added; + Set committed = getCommittedFiles().keySet(); + return new ImmutableSet.Builder().addAll(added).addAll(committed).build(); + } + + @NotNull + private Map getCommittedFiles() throws IOException { + Map committed = readCommit(index.commit_id).files; + return Maps.filterKeys(committed, path -> !index.removed.contains(path)); + } + + private static class Index implements Serializable { + @NotNull + private Set added = new TreeSet<>(); + @NotNull + private Set removed = new TreeSet<>(); + @Nullable + private VcsBranch branch; + private long commit_id = VcsCommit.nullCommit.id; + + Index(long commit_id) { + this.commit_id = commit_id; + this.branch = null; + } + + Index(@NotNull VcsBranch branch) { + this.branch = branch; + this.commit_id = branch.commit_id; + } + } +} diff --git a/myVCS/src/main/java/com/maxim/vcs_objects/VcsBlob.java b/myVCS/src/main/java/com/maxim/vcs_objects/VcsBlob.java new file mode 100644 index 0000000..49dc035 --- /dev/null +++ b/myVCS/src/main/java/com/maxim/vcs_objects/VcsBlob.java @@ -0,0 +1,19 @@ +package com.maxim.vcs_objects; + +import org.apache.commons.codec.digest.DigestUtils; +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; + +/** + * stores file, it's hash + */ +public class VcsBlob implements Serializable { + public final @NotNull byte[] bytes; + public final @NotNull String md5_hash; + + public VcsBlob(@NotNull byte[] bytes) { + this.bytes = bytes; + md5_hash = DigestUtils.md5Hex(bytes); + } +} diff --git a/myVCS/src/main/java/com/maxim/vcs_objects/VcsBlobLink.java b/myVCS/src/main/java/com/maxim/vcs_objects/VcsBlobLink.java new file mode 100644 index 0000000..aa15c98 --- /dev/null +++ b/myVCS/src/main/java/com/maxim/vcs_objects/VcsBlobLink.java @@ -0,0 +1,30 @@ +package com.maxim.vcs_objects; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.Serializable; + +/** + * Stores md5 hash of file + */ +public class VcsBlobLink implements Comparable, Serializable { + public final @NotNull String md5_hash; + + public VcsBlobLink(@NotNull String md5_hash) { + this.md5_hash = md5_hash; + } + + @Override + public boolean equals(@Nullable Object other) { + return !(other == null || !(other instanceof VcsBlobLink)) && md5_hash.equals(((VcsBlobLink) other).md5_hash); + } + + @Override + public int compareTo(@Nullable Object other) { + if (other == null || !(other instanceof VcsBlobLink)) { + return -1; + } + return md5_hash.compareTo(((VcsBlobLink) other).md5_hash); + } +} diff --git a/myVCS/src/main/java/com/maxim/vcs_objects/VcsBranch.java b/myVCS/src/main/java/com/maxim/vcs_objects/VcsBranch.java new file mode 100644 index 0000000..c509f80 --- /dev/null +++ b/myVCS/src/main/java/com/maxim/vcs_objects/VcsBranch.java @@ -0,0 +1,23 @@ +package com.maxim.vcs_objects; + +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; + +/** + * Stores name of branch, and head commit id + */ +public class VcsBranch implements Serializable { + @NotNull + public final String name; + public final long commit_id; + + public VcsBranch(@NotNull String name, long commit_id) { + this.name = name; + this.commit_id = commit_id; + } + + public VcsBranch changeCommit(long commit_id) { + return new VcsBranch(name, commit_id); + } +} diff --git a/myVCS/src/main/java/com/maxim/vcs_objects/VcsCommit.java b/myVCS/src/main/java/com/maxim/vcs_objects/VcsCommit.java new file mode 100644 index 0000000..bdf5f77 --- /dev/null +++ b/myVCS/src/main/java/com/maxim/vcs_objects/VcsCommit.java @@ -0,0 +1,53 @@ +package com.maxim.vcs_objects; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; +import java.util.*; + +/** + * Represent commit: + * stores id, message, date, parent commits id, links to committed files + */ + +public class VcsCommit implements Serializable { + public final long id; + public final @NotNull String message; + public final long date; + public final @NotNull List parents_ids; + public final @NotNull Map files; + public final static VcsCommit nullCommit = new VcsCommit(); + + public VcsCommit(@NotNull String message, @NotNull List parents_ids, @NotNull Map files) { + id = Math.abs(new Random().nextLong()); + this.message = message; + this.parents_ids = ImmutableList.copyOf(parents_ids); + this.files = ImmutableMap.copyOf(files); + this.date = new Date().getTime(); + } + + @NotNull + @Override + public String toString() { + return String.valueOf(id) + "\n" + + " message: " + message + "\n" + + " date:" + new Date(date) + "\n" + + " parents: " + parents_ids + "\n" + + "\n"; + } + + private VcsCommit() { + id = 0; + this.message = "initial"; + this.parents_ids = ImmutableList.of(); + this.files = ImmutableMap.of(); + this.date = 0; + } + + @Override + public boolean equals(Object other) { + return !(other == null || !(other instanceof VcsCommit)) && id == ((VcsCommit) other).id; + } +} diff --git a/myVCS/src/test/java/com/maxim/vcs_impl/VcsImplTest.java b/myVCS/src/test/java/com/maxim/vcs_impl/VcsImplTest.java new file mode 100644 index 0000000..0e9086c --- /dev/null +++ b/myVCS/src/test/java/com/maxim/vcs_impl/VcsImplTest.java @@ -0,0 +1,261 @@ +package com.maxim.vcs_impl; + +import com.maxim.vcs_objects.VcsCommit; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.function.Consumer; +import java.util.function.Function; + +import static junit.framework.TestCase.assertFalse; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +public class VcsImplTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void addTest1() throws Exception { + final Vcs vcs = new VcsImpl(Paths.get(temporaryFolder.getRoot().getPath())); + List paths = initFolder(); + vcs.createBranch("branch"); + + Callable> getAdded = () -> { + Map < Path, String > cur_status = vcs.status(); + List added = new ArrayList<>(); + cur_status.forEach((path, state) -> { + if (state.equals("added")) { + added.add(path); + } + }); + return added; + }; + + vcs.add(paths.get(0)); + vcs.add(paths.get(1)); + assertThat(getAdded.call(), containsInAnyOrder(paths.get(0), paths.get(1))); + vcs.commit("message"); + assertThat(getAdded.call().size(), equalTo(0)); + vcs.add(paths.get(0)); + vcs.add(paths.get(2)); + assertThat(getAdded.call(), containsInAnyOrder( paths.get(2))); + } + + @Test(expected = IOException.class) + public void addTest2() throws Exception { + final Vcs vcs = new VcsImpl(Paths.get(temporaryFolder.getRoot().getPath())); + List paths = initFolder(); + vcs.add(Paths.get(paths.get(0) + "", "abrcadabra")); + } + + @Test + public void logTest() throws IOException { + final Vcs vcs = new VcsImpl(Paths.get(temporaryFolder.getRoot().getPath())); + List paths = initFolder(); + + assertThat(vcs.getCurrentBranchName(), equalTo("null")); + assertThat(vcs.logBranches().size(), equalTo(0)); + + vcs.createBranch("master"); + vcs.createBranch("develop"); + + assertThat(vcs.logBranches(), containsInAnyOrder("master", "develop")); + assertThat(vcs.getCurrentBranchName(), equalTo("develop")); + + assertThat(vcs.logCommits(), containsInAnyOrder(VcsCommit.nullCommit)); + + List commits = new ArrayList<>(); + commits.add(VcsCommit.nullCommit.id); + + for (int i = 0; i < 10; i++) { + commits.add(vcs.commit("message").id); + } + + assertThat(vcs.logCommits().size(), equalTo(commits.size())); + + vcs.merge("master"); + assertThat(vcs.logBranches(), containsInAnyOrder("develop")); + } + + @Test + public void commitTest1() throws IOException { + final Vcs vcs = new VcsImpl(Paths.get(temporaryFolder.getRoot().getPath())); + List paths = initFolder(); + vcs.createBranch("master"); + Path path1 = paths.get(0); + Date date1 = new Date(); + vcs.add(path1); + VcsCommit commit1 = vcs.commit("Hello, world"); + assertThat(commit1.message, equalTo("Hello, world")); + assertThat(commit1.parents_ids, containsInAnyOrder(0L)); + assertThat(commit1.files.keySet(), containsInAnyOrder(path1.toString())); + assertTrue(commit1.date > date1.getTime()); + } + + @Test(expected=IOException.class) + public void commitTest2() throws IOException { + final Vcs vcs = new VcsImpl(Paths.get(temporaryFolder.getRoot().getPath())); + vcs.commit("message"); + } + + @Test + public void checkoutTest() throws IOException { + final Vcs vcs = new VcsImpl(Paths.get(temporaryFolder.getRoot().getPath())); + List paths = initFolder(); + Path path = paths.get(0); + vcs.createBranch("master"); + List commits = new ArrayList<>(); + final int n = 10; + for (long i = 0; i < n; i++) { + FileUtil.writeObject(i, path); + vcs.add(path); + commits.add(vcs.commit("Hi")); + if (i == 0) { + vcs.createBranch("dev"); + } + } + for (int i = 0; i < n; i++) { + vcs.checkoutCommit(commits.get(i).id); + Long value = (Long) FileUtil.readObject(path); + assertEquals(Long.valueOf(i), value); + } + vcs.checkoutBranch("master"); + assertEquals(0L, FileUtil.readObject(path)); + } + + @Test + public void mergeTest() throws IOException { + final Vcs vcs = new VcsImpl(Paths.get(temporaryFolder.getRoot().getPath())); + List paths = initFolder(); + Path path = paths.get(0); + class InitBranch implements Function { + final int value; + final Vcs vcs; + + public InitBranch(int value, Vcs vcs) { + this.value = value; + this.vcs = vcs; + } + + @Override + public VcsCommit apply(String name) { + try { + vcs.checkoutCommit(VcsCommit.nullCommit.id); + vcs.createBranch(name); + FileUtil.writeObject(value, path); + vcs.add(path); + return vcs.commit("commit"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + VcsCommit commit1 = new InitBranch(10, vcs).apply("dev"); + VcsCommit commit2 = new InitBranch(10, vcs).apply("test"); + VcsCommit commit = vcs.merge("dev"); + assertThat(commit.parents_ids, containsInAnyOrder(commit1.id, commit2.id)); + assertEquals("test", vcs.getCurrentBranchName()); + try { + new InitBranch(11, vcs).apply("dev2"); + vcs.merge("test"); + fail(); + } catch (Exception e) { + assertEquals(IOException.class, e.getClass()); + } + } + + @Test + public void resetTest() throws IOException { + final Vcs vcs = new VcsImpl(Paths.get(temporaryFolder.getRoot().getPath())); + List paths = initFolder(); + Path path = paths.get(0); + vcs.createBranch("master"); + FileUtil.writeObject("hello", path); + vcs.add(path); + vcs.commit(""); + FileUtil.writeObject("hello!", path); + vcs.reset(path); + assertThat(FileUtil.readObject(path), equalTo("hello")); + } + + @Test + public void statusTest() throws IOException { + final Vcs vcs = new VcsImpl(Paths.get(temporaryFolder.getRoot().getPath())); + List paths = initFolder(); + vcs.createBranch("master"); + + vcs.add(paths.get(0)); + vcs.add(paths.get(1)); + assertThat(vcs.status().values(), containsInAnyOrder("added", "added", "untracked", "untracked")); + + vcs.commit(""); + assertThat(vcs.status().values(), containsInAnyOrder("committed", "committed", "untracked", "untracked")); + + vcs.rm(paths.get(0)); + assertThat(vcs.status().values(), containsInAnyOrder("removed", "committed", "untracked", "untracked")); + + FileUtil.writeObject("hey", paths.get(1)); + assertThat(vcs.status().values(), containsInAnyOrder("removed", "modified", "untracked", "untracked")); + } + + @Test + public void rmTest() throws IOException { + final Vcs vcs = new VcsImpl(Paths.get(temporaryFolder.getRoot().getPath())); + List paths = initFolder(); + vcs.rm(paths.get(3)); + assertFalse(Files.exists(paths.get(3))); + + vcs.createBranch("master"); + vcs.add(paths.get(0)); + VcsCommit commit = vcs.commit(""); + vcs.rm(paths.get(0)); + vcs.add(paths.get(1)); + vcs.commit(""); + assertFalse(Files.exists(paths.get(0))); + + vcs.checkoutCommit(commit.id); + assertTrue(Files.exists(paths.get(0))); + assertFalse(Files.exists(paths.get(1))); + } + + @Test + public void cleanTest() throws IOException { + final Vcs vcs = new VcsImpl(Paths.get(temporaryFolder.getRoot().getPath())); + List paths = initFolder(); + + vcs.createBranch("master"); + vcs.add(paths.get(0)); + vcs.add(paths.get(2)); + vcs.commit(""); + vcs.clean(); + + assertThat(vcs.status().values(), containsInAnyOrder("committed", "committed")); + } + + private List initFolder() throws IOException { + List res = new ArrayList<>(); + res.add(Paths.get(temporaryFolder.getRoot().getPath(), "1")); + FileUtil.writeObject(1, res.get(0)); + + File folder = temporaryFolder.newFolder("folder"); + for (int i = 2; i <= 4; i++) { + res.add(Paths.get(folder.getPath(), i + "")); + FileUtil.writeObject(i, res.get(res.size() - 1)); + } + return res; + } +} \ No newline at end of file