diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b7c1c7e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build +.idea \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a1866fa --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: java + +jdk: +- oraclejdk8 \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..6a927bc --- /dev/null +++ b/build.gradle @@ -0,0 +1,15 @@ +group 'ru.spbau.shevchenko' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.11' + compile group: 'org.jetbrains', name: 'annotations', version: '13.0' +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..ca78035 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..e785657 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Mar 21 12:37:10 MSK 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..27309d9 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/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" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..832fdb6 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@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 +if "%@eval[2+2]" == "4" goto 4NT_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=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +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..c271805 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'VCS' +rootProject.name = 'VCS' + diff --git a/src/main/java/console/Main.java b/src/main/java/console/Main.java new file mode 100644 index 0000000..4951493 --- /dev/null +++ b/src/main/java/console/Main.java @@ -0,0 +1,97 @@ +package console; + +import vcs.EmptyCommitMessageException; +import vcs.NothingToCommitException; +import vcs.VCS; +import vcs.VCSException; + +import java.io.IOException; +import java.util.List; + +public class Main { + public static void main(String[] args) { + for (String arg : args) { + System.out.println(arg); + } + VCS vcs; + try { + vcs = new VCS(); + } catch (Exception e) { + System.out.println(e.getMessage()); + return; + } + switch (args[0]) { + case "add": { + try { + vcs.add(args[1]); + } catch (IOException e) { + printException(e); + } + break; + } + case "commit": { + try { + vcs.commit(args[1]); + } catch (Exception e) { + printException(e); + } + break; + } + case "branch": { + if (args[1].equals("-d")) { + try { + vcs.deleteBranch(args[2]); + } catch (VCSException | IOException e) { + printException(e); + } + } + else { + try { + vcs.createBranch(args[1]); + } catch (Exception e) { + printException(e); + } + } + break; + } + case "log": { + try { + List log = vcs.log(); + for (String record : log) { + System.out.print(record); + System.out.println("\n---------------------\n"); + } + } catch (IOException | ClassNotFoundException e) { + printException(e); + } + break; + } + case "checkout": { + try { + vcs.checkout(args[1]); + } catch (Exception e) { + printException(e); + } + break; + } + case "merge": { + try { + List conflicts = vcs.merge(args[1]); + if (conflicts.isEmpty()) { + System.out.println("Merged successfully"); + } + else { + System.out.println("Following files conflict:"); + conflicts.forEach(System.out::println); + } + } catch (Exception e) { + printException(e); + } + } + } + } + + private static void printException(Exception e) { + System.out.println(e.getClass().toString() + " " + e.getMessage()); + } +} diff --git a/src/main/java/console/package-info.java b/src/main/java/console/package-info.java new file mode 100644 index 0000000..b804d98 --- /dev/null +++ b/src/main/java/console/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides Main class for console interface. + */ +package console; \ No newline at end of file diff --git a/src/main/java/vcs/Blob.java b/src/main/java/vcs/Blob.java new file mode 100644 index 0000000..c2ebbbe --- /dev/null +++ b/src/main/java/vcs/Blob.java @@ -0,0 +1,14 @@ +package vcs; + +import java.io.IOException; +import java.io.Serializable; +import java.nio.file.Path; +import java.nio.file.Paths; + +interface Blob extends GitObject, Serializable{ + Path BLOB_DIR = Paths.get("blobs"); + + String getPath(); + ContentfulBlob getContentfulBlob() throws IOException, ClassNotFoundException; + ContentlessBlob getContentlessBlob(); +} diff --git a/src/main/java/vcs/BlobSHARef.java b/src/main/java/vcs/BlobSHARef.java new file mode 100644 index 0000000..08f7179 --- /dev/null +++ b/src/main/java/vcs/BlobSHARef.java @@ -0,0 +1,37 @@ +package vcs; + +import java.io.IOException; + +class BlobSHARef implements SHARef { + private String blobSHA; + + BlobSHARef(String blobSHA) { + this.blobSHA = blobSHA; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + BlobSHARef that = (BlobSHARef) o; + + return blobSHA != null ? blobSHA.equals(that.blobSHA) : that.blobSHA == null; + + } + + @Override + public int hashCode() { + return blobSHA != null ? blobSHA.hashCode() : 0; + } + + @Override + public GitObject getObject() throws IOException, ClassNotFoundException { + return (GitObject) VCSFiles.readObject(Blob.BLOB_DIR.resolve(blobSHA)); + } + + @Override + public String toString() { + return blobSHA; + } +} diff --git a/src/main/java/vcs/Branch.java b/src/main/java/vcs/Branch.java new file mode 100644 index 0000000..f8a0e54 --- /dev/null +++ b/src/main/java/vcs/Branch.java @@ -0,0 +1,58 @@ +package vcs; + +import java.io.IOException; +import java.io.Serializable; + +class Branch implements CommitRef, Serializable { + private CommitSHARef headCommit; + private String name; + + + Branch(String name, CommitSHARef headCommit) { + this.name = name; + this.headCommit = headCommit; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Branch branch = (Branch) o; + + return name.equals(branch.name); + + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public CommitSHARef getCommitSHA() { + return headCommit.getCommitSHA(); + } + + @Override + public Commit getCommit() throws IOException, ClassNotFoundException { + return headCommit.getCommit(); + } + + /** + * + * @param commit to be added after + * @return new head + */ + @Override + public CommitRef addCommitAfter(Commit commit) throws IOException { + headCommit = commit.getSHARef(); + writeToDisk(); + return this; + } + + public void writeToDisk() throws IOException { + VCSFiles.writeObject(Branches.BRANCHES_DIR.resolve(name), this); + } + +} diff --git a/src/main/java/vcs/BranchAlreadyExistsException.java b/src/main/java/vcs/BranchAlreadyExistsException.java new file mode 100644 index 0000000..e43ef05 --- /dev/null +++ b/src/main/java/vcs/BranchAlreadyExistsException.java @@ -0,0 +1,10 @@ +package vcs; + +/** + * Signals an attempt to create branch with name that is already taken. + */ +class BranchAlreadyExistsException extends VCSException { + BranchAlreadyExistsException(String s) { + super(s); + } +} diff --git a/src/main/java/vcs/BranchNotFoundException.java b/src/main/java/vcs/BranchNotFoundException.java new file mode 100644 index 0000000..e099fde --- /dev/null +++ b/src/main/java/vcs/BranchNotFoundException.java @@ -0,0 +1,10 @@ +package vcs; + +/** + * Signals an attempt to reference branch that does not exist. + */ +public class BranchNotFoundException extends VCSException { + BranchNotFoundException(String branchName) { + super("Branch with name " + branchName + " doesn't exist!"); + } +} diff --git a/src/main/java/vcs/Branches.java b/src/main/java/vcs/Branches.java new file mode 100644 index 0000000..90d81d2 --- /dev/null +++ b/src/main/java/vcs/Branches.java @@ -0,0 +1,46 @@ +package vcs; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +class Branches { + static final Path BRANCHES_DIR = Paths.get("branches"); + static boolean exists(String branchName) { + return VCSFiles.exists(getPath(branchName)); + } + static Branch create(String branchName, CommitSHARef headCommit) throws IOException, BranchAlreadyExistsException { + if (exists(branchName)) { + throw new BranchAlreadyExistsException(branchName); + } + Path branchPath = getPath(branchName); + VCSFiles.create(branchPath); + Branch newBranch = new Branch(branchName, headCommit); + VCSFiles.writeObject(branchPath, newBranch); + return newBranch; + } + static void delete(String branchName) throws IOException, BranchNotFoundException { + if (!exists(branchName)) { + throw new BranchNotFoundException(branchName); + } + VCSFiles.delete(getPath(branchName)); + } + + @NotNull + static Branch get(String branchName) throws BranchNotFoundException, IOException, VCSFilesCorruptedException { + if (!exists(branchName)) { + throw new BranchNotFoundException(branchName); + } + try { + return (Branch) VCSFiles.readObject(getPath(branchName)); + } catch (ClassNotFoundException e) { + throw new VCSFilesCorruptedException(); + } + } + + private static Path getPath(String branchName) { + return BRANCHES_DIR.resolve(branchName); + } +} diff --git a/src/main/java/vcs/CheckoutStagedNotEmptyException.java b/src/main/java/vcs/CheckoutStagedNotEmptyException.java new file mode 100644 index 0000000..a3ef98f --- /dev/null +++ b/src/main/java/vcs/CheckoutStagedNotEmptyException.java @@ -0,0 +1,4 @@ +package vcs; + +class CheckoutStagedNotEmptyException extends VCSException { +} diff --git a/src/main/java/vcs/Commit.java b/src/main/java/vcs/Commit.java new file mode 100644 index 0000000..493fb60 --- /dev/null +++ b/src/main/java/vcs/Commit.java @@ -0,0 +1,65 @@ +package vcs; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +class Commit implements GitObject { + + private static final Path COMMIT_DIR = Paths.get("commits"); + @NotNull + private List files; + private CommitSHARef prevCommit; + private long time; + private String message; + + Commit(String message, @NotNull List files, CommitSHARef prevCommit) throws IOException { + this.message = message; + this.files = files; + this.time = System.currentTimeMillis(); + this.prevCommit = prevCommit; + VCSFiles.writeObject(COMMIT_DIR.resolve(getSHA()), this); + } + + @Override + public String toString() { + @SuppressWarnings("StringBufferReplaceableByString") StringBuilder result = new StringBuilder(getSHA() + "\n"); + result.append(message); + return result.toString(); + } + + CommitSHARef getPrevCommit() { + return prevCommit; + } + + private String getSHA() { + try { + MessageDigest messageDigest = MessageDigest.getInstance(SHA1); + messageDigest.update(String.valueOf(time).getBytes()); + messageDigest.update(message.getBytes()); + return GitObject.byteArrayToHex(messageDigest.digest()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-1 hashing algorithm not implemented!"); + } + } + + static Commit get(String revision) throws IOException, ClassNotFoundException { + return (Commit) VCSFiles.readObject(COMMIT_DIR.resolve(revision)); + } + + @Override + public CommitSHARef getSHARef() { + return new CommitSHARef(getSHA()); + } + + @NotNull + List getFiles() { + return files; + } + +} diff --git a/src/main/java/vcs/CommitRef.java b/src/main/java/vcs/CommitRef.java new file mode 100644 index 0000000..7adda78 --- /dev/null +++ b/src/main/java/vcs/CommitRef.java @@ -0,0 +1,16 @@ +package vcs; + +import java.io.IOException; +import java.io.Serializable; +import java.nio.file.Paths; + +public interface CommitRef extends Serializable{ + static CommitRef readRef(String filename) throws IOException, ClassNotFoundException { + return (CommitRef) VCSFiles.readObject(Paths.get(filename)); + } + + CommitSHARef getCommitSHA(); + Commit getCommit() throws IOException, ClassNotFoundException; + CommitRef addCommitAfter(Commit commit) throws IOException; + +} diff --git a/src/main/java/vcs/CommitSHARef.java b/src/main/java/vcs/CommitSHARef.java new file mode 100644 index 0000000..cd699f1 --- /dev/null +++ b/src/main/java/vcs/CommitSHARef.java @@ -0,0 +1,46 @@ +package vcs; + + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +class CommitSHARef implements CommitRef, SHARef{ + private static final Path COMMIT_DIR = Paths.get("commits"); + private String commitSHA; + + CommitSHARef(String commitSHA) { + this.commitSHA = commitSHA; + } + + @Override + public CommitSHARef getCommitSHA() { + return this; + } + + @Override + public Commit getCommit() throws IOException, ClassNotFoundException { + return (Commit) VCSFiles.readObject(COMMIT_DIR.resolve(commitSHA)); + } + + /** + * Adds commit after the one reference points to. + * @param commit commit to be added after the one reference points to + * @return reference to added commit + */ + @Override + public CommitRef addCommitAfter(Commit commit) { + return commit.getSHARef(); + } + + + @Override + public String toString() { + return commitSHA; + } + + @Override + public GitObject getObject() throws IOException, ClassNotFoundException { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/vcs/ContentfulBlob.java b/src/main/java/vcs/ContentfulBlob.java new file mode 100644 index 0000000..b980943 --- /dev/null +++ b/src/main/java/vcs/ContentfulBlob.java @@ -0,0 +1,57 @@ +package vcs; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +class ContentfulBlob implements Blob{ + private String path; + @NotNull + private byte[] content; + + private String blobSHA; + + ContentfulBlob(Path filePath) throws IOException { + content = Files.readAllBytes(filePath); + path = filePath.toString(); + try { + MessageDigest messageDigest = MessageDigest.getInstance(SHA1); + messageDigest.update(path.getBytes()); + messageDigest.update(content); + blobSHA = GitObject.byteArrayToHex(messageDigest.digest()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-1 hashing algorithm not implemented!"); + } + VCSFiles.writeObject(BLOB_DIR.resolve(blobSHA), this); + } + + + @Override + public BlobSHARef getSHARef() { + return new BlobSHARef(blobSHA); + } + + @Override + public String getPath() { + return path; + } + + @Override + public ContentfulBlob getContentfulBlob() { + return this; + } + + @Override + public ContentlessBlob getContentlessBlob() { + return new ContentlessBlob(path, getSHARef()); + } + + @NotNull + byte[] getContent() { + return content; + } +} diff --git a/src/main/java/vcs/ContentlessBlob.java b/src/main/java/vcs/ContentlessBlob.java new file mode 100644 index 0000000..2ccb018 --- /dev/null +++ b/src/main/java/vcs/ContentlessBlob.java @@ -0,0 +1,58 @@ +package vcs; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +class ContentlessBlob implements Blob{ + @NotNull + private String path; + @NotNull + private BlobSHARef blobRef; + + ContentlessBlob(@NotNull String path, @NotNull BlobSHARef ref) { + + this.path = path; + this.blobRef = ref; + } + + @Override + public BlobSHARef getSHARef() { + return blobRef; + } + + @NotNull + @Override + public String getPath() { + return path; + } + + @NotNull + @Override + public ContentfulBlob getContentfulBlob() throws IOException, ClassNotFoundException { + return (ContentfulBlob) VCSFiles.readObject(BLOB_DIR.resolve(blobRef.toString())); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ContentlessBlob that = (ContentlessBlob) o; + + return blobRef.equals(that.blobRef); + + } + + @Override + public int hashCode() { + int result = path.hashCode(); + result = 31 * result + blobRef.hashCode(); + return result; + } + + @Override + public ContentlessBlob getContentlessBlob() { + return this; + } +} diff --git a/src/main/java/vcs/DeleteActiveBranchException.java b/src/main/java/vcs/DeleteActiveBranchException.java new file mode 100644 index 0000000..466a5b0 --- /dev/null +++ b/src/main/java/vcs/DeleteActiveBranchException.java @@ -0,0 +1,10 @@ +package vcs; + +/** + * Signals an attempt to delete branch to which HEAD points. + */ +public class DeleteActiveBranchException extends VCSException { + DeleteActiveBranchException(String s) { + super(s); + } +} diff --git a/src/main/java/vcs/EmptyCommitMessageException.java b/src/main/java/vcs/EmptyCommitMessageException.java new file mode 100644 index 0000000..83561d1 --- /dev/null +++ b/src/main/java/vcs/EmptyCommitMessageException.java @@ -0,0 +1,4 @@ +package vcs; + +public class EmptyCommitMessageException extends VCSException { +} diff --git a/src/main/java/vcs/GitObject.java b/src/main/java/vcs/GitObject.java new file mode 100644 index 0000000..7580164 --- /dev/null +++ b/src/main/java/vcs/GitObject.java @@ -0,0 +1,13 @@ +package vcs; + +import java.io.Serializable; +import java.math.BigInteger; + +interface GitObject extends Serializable{ + String SHA1 = "SHA-1"; + SHARef getSHARef(); + + static String byteArrayToHex(byte[] array) { + return (new BigInteger(1, array)).toString(16); + } +} diff --git a/src/main/java/vcs/MergeWhenStagedNotEmptyException.java b/src/main/java/vcs/MergeWhenStagedNotEmptyException.java new file mode 100644 index 0000000..0573075 --- /dev/null +++ b/src/main/java/vcs/MergeWhenStagedNotEmptyException.java @@ -0,0 +1,4 @@ +package vcs; + +class MergeWhenStagedNotEmptyException extends VCSException { +} diff --git a/src/main/java/vcs/NothingToCommitException.java b/src/main/java/vcs/NothingToCommitException.java new file mode 100644 index 0000000..d778ad5 --- /dev/null +++ b/src/main/java/vcs/NothingToCommitException.java @@ -0,0 +1,7 @@ +package vcs; + +/** + * Signals an attempt to commit when no files were staged for commit. + */ +public class NothingToCommitException extends VCSException { +} diff --git a/src/main/java/vcs/RepoState.java b/src/main/java/vcs/RepoState.java new file mode 100644 index 0000000..4b2c99b --- /dev/null +++ b/src/main/java/vcs/RepoState.java @@ -0,0 +1,85 @@ +package vcs; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +class RepoState { + private HashMap files; + private Path filePath = null; + + private RepoState() { + files = new HashMap<>(); + } + RepoState(String stateName) throws IOException, ClassNotFoundException { + filePath = Paths.get(stateName); + if (VCSFiles.exists(filePath)) { + //noinspection unchecked + files = (HashMap) VCSFiles.readObject(filePath); + } + else { + files = new HashMap<>(); + VCSFiles.create(filePath); + VCSFiles.writeObject(filePath, files); + } + } + + void add(ContentlessBlob blob) throws IOException { + if (files.containsKey(blob.getPath())) { + return; + } + files.put(blob.getPath(), blob); + flush(); + } + @SuppressWarnings("unused") + public void remove(ContentlessBlob blob) throws IOException { + if (!files.containsKey(blob)) { + return; // TODO: throw exception? + } + files.remove(blob.getPath()); + flush(); + } + + boolean empty() { + return files.isEmpty(); + } + + void updateWith(RepoState delta) throws IOException { + for (Map.Entry entry : delta.getFiles().entrySet()) { + files.put(entry.getKey(), entry.getValue()); + } + flush(); + } + + HashMap getFiles() { + return files; + } + + static RepoState getFromCommit(Commit commit) throws IOException, ClassNotFoundException { + RepoState result = new RepoState(); + while (commit != null) { + List blobs = commit.getFiles(); + for (ContentlessBlob blob : blobs) { + result.add(blob); + } + if (commit.getPrevCommit() == null) { + break; + } + commit = commit.getPrevCommit().getCommit(); + } + return result; + } + + void clear() throws IOException { + files.clear(); + flush(); + } + + private void flush() throws IOException { + if (filePath != null) { + VCSFiles.writeObject(filePath, files); + } + } +} diff --git a/src/main/java/vcs/SHARef.java b/src/main/java/vcs/SHARef.java new file mode 100644 index 0000000..c5a55e6 --- /dev/null +++ b/src/main/java/vcs/SHARef.java @@ -0,0 +1,9 @@ +package vcs; + +import java.io.IOException; +import java.io.Serializable; + +interface SHARef extends Serializable{ + @SuppressWarnings("unused") + GitObject getObject() throws IOException, ClassNotFoundException; +} diff --git a/src/main/java/vcs/VCS.java b/src/main/java/vcs/VCS.java new file mode 100644 index 0000000..f489b64 --- /dev/null +++ b/src/main/java/vcs/VCS.java @@ -0,0 +1,202 @@ +package vcs; + +import java.io.IOException; +import java.nio.file.*; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Class that contains all VCS interface methods. + */ + +public class VCS { + private static final String INDEX_FILENAME = "index"; + private static final String STAGED_FILENAME = "staged"; + private static final String HEAD_FILENAME = "HEAD"; + private static final byte[] MERGED_FILES_SEPARATOR = "\n===============\n".getBytes(); + private static final String MASTER_BRANCH = "master"; + private CommitRef head; + private RepoState index; + private RepoState staged; + + // TODO: take String[] as args and check them + + /** + * Creates new VCS instance in current directory or loads one if it already exists. + * @throws VCSFilesCorruptedException if VCS files are absent or corrupted + * @throws IOException if for some reason VCS files cannot be read or written + */ + public VCS() throws IOException, VCSFilesCorruptedException { + VCSFiles.init(); + try { + index = new RepoState(INDEX_FILENAME); + staged = new RepoState(STAGED_FILENAME); + } catch (ClassNotFoundException e) { + throw new VCSFilesCorruptedException(); + } + Path headPath = Paths.get(HEAD_FILENAME); + if (!VCSFiles.exists(headPath)) { + VCSFiles.create(headPath); + Commit initial = new Commit("initial", new ArrayList<>(), null); + try { + VCSFiles.writeObject(headPath, Branches.create(MASTER_BRANCH, initial.getSHARef())); + } catch (BranchAlreadyExistsException e) { + throw new VCSFilesCorruptedException(); + } + } + try { + head = CommitRef.readRef(HEAD_FILENAME); + } catch (ClassNotFoundException e) { + throw new VCSFilesCorruptedException(); + } + } + + /** + * Sets HEAD to revison and makes files consistent with revision state. + * @param revision name of a commit or a branch to checkout to + * @throws IOException if for some reason VCS files cannot be read or written + * @throws BranchNotFoundException if passed revision is branch and there's no such branch + * @throws VCSFilesCorruptedException if VCS files are absent or corrupted + */ + public void checkout(String revision) throws CheckoutStagedNotEmptyException, VCSFilesCorruptedException, + BranchNotFoundException, IOException, ClassNotFoundException { + if (!staged.empty()) { + throw new CheckoutStagedNotEmptyException(); + } + if (Branches.exists(revision)) { + Branch newBranch = Branches.get(revision); + head = newBranch; + index = RepoState.getFromCommit(newBranch.getCommit()); + } + else { + Commit headCommit = Commit.get(revision); + head = headCommit.getSHARef(); + index = RepoState.getFromCommit(headCommit); + } + // TODO: move to method of RepoState + HashMap indexFiles = index.getFiles(); + for (Map.Entry blob : indexFiles.entrySet()) { + byte[] content = blob.getValue().getContentfulBlob().getContent(); + VCSFiles.writeToRoot(Paths.get(blob.getKey()), content); + } + VCSFiles.writeObject(Paths.get(HEAD_FILENAME), head); + } + /** + * Adds file to stage for the next commit. + * @param pathString file to add + * @throws IOException if for some reason VCS files cannot be read or written + */ + public void add(String pathString) throws IOException { + Path filePath = Paths.get(pathString); + if (Files.isDirectory(filePath)) { + throw new UnsupportedOperationException(); + } + ContentfulBlob newBlob = new ContentfulBlob(filePath); + staged.add(newBlob.getContentlessBlob()); + } + /** + * Creates new branch pointing to same commit as HEAD. + * @param branchName name for a newly created branch + * @throws IOException if for some reason VCS files cannot be read or written + * @throws BranchAlreadyExistsException if branchName already exists + */ + public void createBranch(String branchName) throws IOException, BranchAlreadyExistsException { + Branches.create(branchName, head.getCommitSHA()); + } + + /** + * Deletes specified branch. + * @param branchName branch to be deleted + * @throws IOException if for some reason VCS files cannot be read or written + * @throws DeleteActiveBranchException if branchName is active branch + * @throws VCSFilesCorruptedException if VCS files are absent or corrupted + * @throws BranchNotFoundException if branchName does not exist + */ public void deleteBranch(String branchName) throws BranchNotFoundException, IOException, + VCSFilesCorruptedException, DeleteActiveBranchException { + if (head.equals(Branches.get(branchName))) { + throw new DeleteActiveBranchException(branchName); + } + Branches.delete(branchName); + } + /** + * Creates new commit from all staged files. + * @param commitMessage a commit message + * @throws NothingToCommitException if there no files staged for commit + * @throws IOException if for some reason VCS files cannot be read or written + */ + public void commit(String commitMessage) throws NothingToCommitException, EmptyCommitMessageException, IOException { + if (commitMessage.isEmpty()) { + throw new EmptyCommitMessageException(); + } + if (staged.empty()) { + throw new NothingToCommitException(); + } + index.updateWith(staged); + Commit commit = new Commit(commitMessage, staged.getFiles().values().stream().collect(Collectors.toList()), head.getCommitSHA()); + staged.clear(); + head = head.addCommitAfter(commit); + } + /** + * Returns list of all commits in the current branch. + * @return list of all commits in the current branch + * @throws IOException if for some reason VCS files cannot be read or written + */ + public List log() throws IOException, ClassNotFoundException { + CommitRef currentCommitRef = head; + List result = new ArrayList<>(); + while (currentCommitRef != null) { + Commit currentCommit = currentCommitRef.getCommit(); + result.add(currentCommit.toString()); + currentCommitRef = currentCommit.getPrevCommit(); + } + Collections.reverse(result); + return result; + } + /** + * Merges branchName into current branch. + * + * Conflicting file contents are merged with separator. + * @param branchName name of branch to be merge into current + * @return list of conflicting paths + * @throws BranchNotFoundException if there's no branch with the specified name + * @throws VCSFilesCorruptedException if one of .vcs/ files is absent or contains corrupted data + * @throws IOException if for some reason VCS files cannot be read or written + */ + public List merge(String branchName) throws MergeWhenStagedNotEmptyException, VCSFilesCorruptedException, + BranchNotFoundException, IOException, ClassNotFoundException { + if (!staged.empty()) { + throw new MergeWhenStagedNotEmptyException(); + } + Branch mergingBranch = Branches.get(branchName); + RepoState curState = index; + RepoState mergingState = RepoState.getFromCommit(mergingBranch.getCommit()); + Map curBlobs = curState.getFiles(); + List mergingBlobs = mergingState.getFiles().values().stream().collect(Collectors.toList()); + + List conflicts = new ArrayList<>(); + for (ContentlessBlob newBlob : mergingBlobs) { + if (curBlobs.containsKey(newBlob.getPath())) { + ContentlessBlob oldBlob = curBlobs.get(newBlob.getPath()); + if (!oldBlob.getSHARef().equals(newBlob.getSHARef())) { + // TODO: Bad from design point of view + mergeBlobs(oldBlob, newBlob); + conflicts.add(oldBlob.getPath()); + } + } + else { + staged.add(newBlob); + } + } + + return conflicts; + } + + private void mergeBlobs(ContentlessBlob oldBlob, ContentlessBlob newBlob) throws IOException, ClassNotFoundException { + // TODO: looks not right + Path path = Paths.get(oldBlob.getPath()); + Files.write(path, oldBlob.getContentfulBlob().getContent()); + Files.write(path, MERGED_FILES_SEPARATOR, StandardOpenOption.APPEND); + Files.write(path, newBlob.getContentfulBlob().getContent(), StandardOpenOption.APPEND); + } +} diff --git a/src/main/java/vcs/VCSException.java b/src/main/java/vcs/VCSException.java new file mode 100644 index 0000000..0895b27 --- /dev/null +++ b/src/main/java/vcs/VCSException.java @@ -0,0 +1,11 @@ +package vcs; + +/** + * Signals that exception inside VCS of some sort occurred. + */ +public class VCSException extends Exception { + VCSException(){} + VCSException(String s) { + super(s); + } +} diff --git a/src/main/java/vcs/VCSFiles.java b/src/main/java/vcs/VCSFiles.java new file mode 100644 index 0000000..dbb387f --- /dev/null +++ b/src/main/java/vcs/VCSFiles.java @@ -0,0 +1,72 @@ +package vcs; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +class VCSFiles { + private static final Path VCS_DIR = Paths.get(".vcs"); + static void delete(Path path) throws IOException { + Files.delete(VCS_DIR.resolve(path)); + } + + + static void init() throws IOException { + if (!Files.exists(VCS_DIR) || !Files.isDirectory(VCS_DIR)) { + Files.createDirectories(VCS_DIR); + } + } + static void create(Path path) throws IOException { + path = VCS_DIR.resolve(path); + if (!Files.exists(path.getParent())) { + Files.createDirectories(path.getParent()); + } + Files.createFile(path); + } + + public static void write(Path path, String s) throws IOException { + write(path, s.getBytes()); + } + public static void write(Path path, byte[] bytes) throws IOException { + writeToRoot(VCS_DIR.resolve(path), bytes); + } + + static boolean exists(Path path) { + path = VCS_DIR.resolve(path); + return Files.exists(path); + } + + static void writeObject(Path path, Serializable object) throws IOException { + path = VCS_DIR.resolve(path); + if (!Files.exists(path.getParent())) { + Files.createDirectories(path.getParent()); + } + FileOutputStream outStream = new FileOutputStream(path.toFile()); + ObjectOutputStream objOutStream = new ObjectOutputStream(outStream); + objOutStream.writeObject(object); + objOutStream.close(); + outStream.close(); + } + + static Object readObject(Path path) throws IOException, ClassNotFoundException { + path = VCS_DIR.resolve(path); + FileInputStream inStream = new FileInputStream(path.toFile()); + ObjectInputStream objInStream = new ObjectInputStream(inStream); + Object object = objInStream.readObject(); + objInStream.close(); + inStream.close(); + return object; + } + + static void writeToRoot(Path path, byte[] content) throws IOException { + Path parentPath = path.getParent(); + if (parentPath != null && !Files.exists(parentPath)) { + Files.createDirectories(parentPath); + } + if (!Files.exists(path)) { + Files.createFile(path); + } + Files.write(path, content); + } +} diff --git a/src/main/java/vcs/VCSFilesCorruptedException.java b/src/main/java/vcs/VCSFilesCorruptedException.java new file mode 100644 index 0000000..ac9a080 --- /dev/null +++ b/src/main/java/vcs/VCSFilesCorruptedException.java @@ -0,0 +1,8 @@ +package vcs; + +/** + * Signals that VCS files are absent when they should be present or their content is corrupted. + */ +public class VCSFilesCorruptedException extends VCSException { + VCSFilesCorruptedException(){} +} diff --git a/src/main/java/vcs/WrongArgumentsException.java b/src/main/java/vcs/WrongArgumentsException.java new file mode 100644 index 0000000..9bb84fa --- /dev/null +++ b/src/main/java/vcs/WrongArgumentsException.java @@ -0,0 +1,7 @@ +package vcs; + +public class WrongArgumentsException extends VCSException { + public WrongArgumentsException(String s) { + super(s); + } +} diff --git a/src/main/java/vcs/package-info.java b/src/main/java/vcs/package-info.java new file mode 100644 index 0000000..11105a7 --- /dev/null +++ b/src/main/java/vcs/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides VCS classes. + */ +package vcs; \ No newline at end of file diff --git a/src/test/java/vcs/VCSTest.java b/src/test/java/vcs/VCSTest.java new file mode 100644 index 0000000..ffee521 --- /dev/null +++ b/src/test/java/vcs/VCSTest.java @@ -0,0 +1,190 @@ +package vcs; + +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class VCSTest { + private static final TestFile A = new TestFile("A.txt", "A text"); + private static final TestFile A1 = new TestFile("A.txt", "A1 text"); + private static final TestFile B = new TestFile("B.txt", "B text"); + private static final TestFile C = new TestFile("C.txt", "C text"); + private static final TestFile A_B = new TestFile("A/B.txt", "A/B.txt text"); + private static final TestFile A_B_C = new TestFile("A/B/C.txt", "A/B/C.txt text"); + + private static final String INITIAL_MSG = "initial"; + private static final String MASTER_BRANCH = "master"; + private static final Path VCS_PATH = Paths.get(".vcs/"); + + /* + 2016 touch a + 2017 touch b + 2018 touch c + 2019 vcs add a + 2021 vcs commit add_A + 2027 vcs log + 2023 vcs add b + 2024 vcs commit add_B + 2027 vcs log + 2025 vcs add c + 2026 vcs commit add_C + 2027 vcs log + */ + + @Test + public void simpleLog() throws Exception, NothingToCommitException { + deleteRecursive(VCS_PATH); + deleteRecursive(TestFile.TEST_DIR); + VCS vcs = new VCS(); + List files = Arrays.asList(A, B, C); + List commitMessages = new ArrayList<>(); + commitMessages.add(INITIAL_MSG); + for (TestFile cur : files) { + cur.create(); + vcs.add(cur.getPath()); + String commitMessage = "add_" + cur.getPath(); + vcs.commit(commitMessage); + commitMessages.add(commitMessage); + + List log = vcs.log(); + for (int i = 0; i < commitMessages.size(); i++) { + compareMessages(commitMessages.get(i), log.get(i)); + } + } + } + + /* + 2031 vcs branch A + 2032 vcs branch B + 2032 vcs branch ะก + + 2033 touch A + 1111 echo "A1" > A + 2034 touch B + 1111 echo "B1" > B + 2035 touch C + 1111 echo "C1" > C + + 2039 vcs checkout A + 2036 vcs add A + 2037 vcs commit add_A + 2038 vcs log + + 2039 vcs checkout B + 2042 vcs add B + 2043 vcs commit add_B + 2052 vcs log + + 2058 vcs checkout C + 2062 vcs add C + 2063 vcs commit add_C + 2064 vcs log + + 2066 rm a b c + 2071 vcs checkout A + 1111 cat A + 2069 vcs checkout B + 1111 cat B + 2069 vcs checkout C + 1111 cat C + + */ + @Test + public void branchesCheckout() throws Exception, NothingToCommitException, BranchAlreadyExistsException { + deleteRecursive(TestFile.TEST_DIR); + deleteRecursive(VCS_PATH); + VCS vcs = new VCS(); + List branches = Arrays.asList("A", "B", "C"); + List files = Arrays.asList(A, B, C); + for (String branch : branches) { + vcs.createBranch(branch); + } + for (int i = 0; i < branches.size(); i++) { + TestFile cur = files.get(i); + String branch = branches.get(i); + cur.create(); + vcs.checkout(branch); + vcs.add(cur.getPath()); + String commitMessage = "add_" + cur.getPath(); + vcs.commit(commitMessage); + List log = vcs.log(); + assertEquals(2, log.size()); + compareMessages(INITIAL_MSG, log.get(0)); + compareMessages(commitMessage, log.get(1)); + } + vcs.checkout(MASTER_BRANCH); + for (TestFile file : files) { + file.remove(); + } + + for (int i = 0; i < branches.size(); i++) { + TestFile cur = files.get(i); + String branch = branches.get(i); + vcs.checkout(branch); + cur.check(); + } + } + + private void compareMessages(String handmadeMessage, String logMessage) { + assertTrue(logMessage.endsWith(handmadeMessage)); + } + + private void deleteRecursive(Path path) throws IOException { + if (Files.notExists(path)) { + return; + } + if (Files.isDirectory(path)) { + DirectoryStream directoryStream = Files.newDirectoryStream(path); + for (Path child : directoryStream) { + deleteRecursive(child); + } + } + Files.delete(path); + } + +} + +class TestFile { + static final Path TEST_DIR = Paths.get("testXX/"); + + private String content; + private Path path; + + TestFile(String path, String content){ + this.content = content; + this.path = TEST_DIR.resolve(path); + } + void create() throws IOException { + Path parent = path.getParent(); + if (parent != null && Files.notExists(parent)) { + Files.createDirectories(parent); + } + if (Files.notExists(path)) { + Files.createFile(path); + } + Files.write(path, content.getBytes()); + } + void remove() throws IOException { + Files.delete(path); + } + boolean check() throws IOException { + return Files.exists(path) && + Arrays.toString(Files.readAllBytes(path)).equals(content); + } + + String getPath() { + return path.toString(); + } +} \ No newline at end of file