package nodebox.localhistory;
import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Calendar;
import java.util.Properties;
public class Repository {
private LocalHistoryManager manager;
private String projectName;
private File directory;
private File projectDirectory;
public Repository(LocalHistoryManager manager, String projectName) {
this.manager = manager;
this.projectName = projectName;
this.directory = new File(manager.getLocalHistoryDirectory(), projectName);
if (!this.directory.exists())
throw new AssertionError("Repository directory " + this.directory + " does not exist.");
// Parse the configuration to set the project directory.
parseConfiguration();
}
/**
* Parse the configuration file, stored under the config directory.
* <p/>
* The configuration file holds the repository format version and the project path.
*/
private void parseConfiguration() {
File configPath = new File(directory, "config");
if (!configPath.exists())
throw new AssertionError("Repository " + projectName + " does not have a config file.");
Properties properties = new Properties();
try {
properties.load(new FileInputStream(configPath));
} catch (IOException e) {
throw new RuntimeException("Could not read config file for repository " + projectName, e);
}
String formatVersion = properties.getProperty("repositoryformatversion");
if (!formatVersion.equals("0"))
throw new AssertionError("Project " + projectName + ": unsupported repository format version.");
projectDirectory = new File(properties.getProperty("projectpath"));
if (!projectDirectory.exists())
throw new AssertionError("Project " + projectName + ": non-existant project directory '" + projectDirectory + "'.");
}
/**
* Given the root directory for a project or library, create a repository under the local history directory.
*
* @param projectDirectory the root of the project
* @return the Repository.
*/
public static Repository create(LocalHistoryManager manager, File projectDirectory) {
if (!projectDirectory.exists())
throw new AssertionError("The project directory '" + projectDirectory + "' does not exist.");
if (!projectDirectory.isDirectory())
throw new AssertionError("The project directory '" + projectDirectory + "' is not a directory.");
String projectName = projectDirectory.getName();
File repositoryDirectory = new File(manager.getLocalHistoryDirectory(), projectName);
File objectsDirectory = new File(repositoryDirectory, "objects");
File refsDirectory = new File(repositoryDirectory, "refs");
File configFile = new File(repositoryDirectory, "config");
if (repositoryDirectory.exists())
throw new AssertionError("A repository named '" + projectName + "' already exists.");
boolean success;
success = repositoryDirectory.mkdir();
if (!success)
throw new RuntimeException("Error while creating repository directory " + repositoryDirectory);
success = objectsDirectory.mkdir();
if (!success)
throw new RuntimeException("Error while creating objects directory " + objectsDirectory);
success = refsDirectory.mkdir();
if (!success)
throw new RuntimeException("Error while creating refs directory " + refsDirectory);
Properties p = new Properties();
p.setProperty("repositoryformatversion", "0");
p.setProperty("projectpath", projectDirectory.getAbsolutePath());
try {
configFile.createNewFile();
FileOutputStream out = new FileOutputStream(configFile);
p.store(out, null);
} catch (IOException e) {
throw new RuntimeException("Error while writing configuration file " + configFile + ".", e);
}
return new Repository(manager, projectName);
}
//// Getters ////
public LocalHistoryManager getManager() {
return manager;
}
public String getProjectName() {
return projectName;
}
public File getProjectDirectory() {
return projectDirectory;
}
public File getDirectory() {
return directory;
}
//// Low-level operations ////
public File objectPath(String id) {
return objectPath(id, false);
}
/**
* Returns the path of the internal object file in the object database.
*
* @param id the object id.
* @param createPath create the path if it does not exist yet.
* @return the path of the object file.
*/
public File objectPath(String id, boolean createPath) {
if (id == null || id.length() != 40)
throw new AssertionError("Invalid id. Use hashObject to get a valid id.");
// The directory is composed of the two first characters of the hash.
String firstTwo = id.substring(0, 2);
String theRest = id.substring(2);
File dirName = new File(directory, "objects/" + firstTwo);
if (createPath && !dirName.exists()) {
if (!dirName.mkdir()) {
throw new AssertionError("Project " + projectName + ": could not create directory '" + dirName + "'.");
}
}
return new File(dirName, theRest);
}
/**
* Returns true if an object with the given hash exists in the object database.
*
* @param id the object id.
* @return true if the object exists.
*/
public boolean objectExists(String id) {
return objectPath(id, false).exists();
}
/**
* Return the data of the repository object with the given id.
*
* @param id the object id.
* @return the data of the object.
*/
public byte[] readObject(String id) {
File objectPath = objectPath(id);
return readFile(objectPath);
// byte[] compressedData = readFile(objectPath);
// Inflater inflater = new Inflater();
// inflater.setInput(compressedData);
// byte[] dataStream = new byte[10000];
// try {
// int decompressedBytes = inflater.inflate(dataStream);
// inflater.end();
// } catch (DataFormatException e) {
// throw new RuntimeException("Data error while decompressing " + id, e);
// }
}
/**
* Return the object id for the given project file.
*
* @param fileName the file to
* @return the object id.
*/
public String hashObject(String fileName) {
return hashObject(fileName, false);
}
/**
* Return the object id for the given project file.
*
* @param fileName the file name to hash
* @param write if true, store the file into the object database.
* @return the object id.
*/
public String hashObject(String fileName, boolean write) {
File fullPath = new File(projectDirectory, fileName);
return hashObject(fullPath, write);
}
/**
* Return the object id for the given project file.
*
* @param file the file to hash
* @param write if true, store the file into the object database.
* @return the object id.
*/
public String hashObject(File file, boolean write) {
if (!file.exists())
throw new AssertionError("File '" + file + "' does not exist.");
if (file.isDirectory())
throw new AssertionError("File '" + file + "' is a directory, which this method cannot hash.");
byte[] data = readFile(file);
if (write)
return writeObject(data);
else
return hashData(data);
}
/**
* Write the data to the object database.
* The object will be stored under its hash, which will be returned.
*
* @param data the data of the object
* @return the object id.
*/
public String writeObject(byte[] data) {
String id = hashData(data);
if (!objectExists(id)) {
File objectPath = objectPath(id, true);
writeFile(objectPath, data, 0, data.length);
// byte[] compressedData = new byte[data.length];
// Deflater d = new Deflater();
// d.setInput(data);
// d.finish();
// int compressedLength = d.deflate(compressedData);
// writeFile(objectPath, compressedData, 0, compressedLength);
}
return id;
}
/**
* Return the number of objects in the object database.
*
* @return the number of objects in the object database.
*/
public int getObjectCount() {
File objectsDirectory = new File(getDirectory(), "objects");
int count = 0;
for (File objectDir : objectsDirectory.listFiles()) {
// Only two-character directory names and names without dots (this avoids .svn directories)
if (objectDir.getName().length() != 2) continue;
if (objectDir.getName().startsWith(".")) continue;
// Include only files with 38 characters, that is files with a hex digest name.
for (File objectFile : objectDir.listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.length() == 38;
}
})) {
count++;
}
}
return count;
}
/**
* Reads a reference from the refs directory.
*
* @param name the name of the reference.
* @return the ID of the reference.
*/
public String readRef(String name) {
File refsDirectory = new File(getDirectory(), "refs");
File refsFile = new File(refsDirectory, name);
if (!refsFile.exists()) return null;
byte[] refBytes = readFile(refsFile);
return new String(refBytes);
}
/**
* Write a reference to the refs directory.
*
* @param name the name of the reference.
* @param id the id of the object it points to.
*/
public void writeRef(String name, String id) {
File refsDirectory = new File(getDirectory(), "refs");
File refsFile = new File(refsDirectory, name);
writeFile(refsFile, id.getBytes(), 0, 40);
}
/**
* Checks if the given reference exists.
*
* @param name the name of the reference.
* @return true if the reference exists.
*/
public boolean refExists(String name) {
File refsDirectory = new File(getDirectory(), "refs");
File refsFile = new File(refsDirectory, name);
return refsFile.exists();
}
//// High-level operations ////
/**
* Store the contents of the working directory in the object database.
* This commit will be stored using an empty message.
*
* @return the id of the commit object.
*/
public String commit() {
return commit("");
}
/**
* Store the contents of the working directory in the object database.
*
* @param message the message for the commit
* @return the id of the commit object.
*/
public String commit(String message) {
StringBuffer commitDataBuffer = new StringBuffer();
// Store the contents of the working directory in the object database.
// This will return the root id of the tree.
String treeId = hashDirectoryRecursive(projectDirectory);
commitDataBuffer.append("tree ").append(treeId).append("\n");
// Find the current head: this will be the parent of this commit.
String parentId = readRef("HEAD");
if (parentId != null) {
commitDataBuffer.append("parent ").append(parentId).append("\n");
}
// Add the commit time
Calendar cal = Calendar.getInstance();
commitDataBuffer.append("time ").append(Commit.dateFormat.format(cal.getTime())).append("\n");
// Append an empty line to indicate the commit message follows
commitDataBuffer.append("\n");
// Add the commit message
commitDataBuffer.append(message);
// Store the commit in the object database.
String commitId = writeObject(commitDataBuffer.toString().getBytes());
//This commit will be the new head. Write a reference to it.
writeRef("HEAD", commitId);
return commitId;
}
public Commit getHead() {
String refId = readRef("HEAD");
if (refId == null)
return null;
return new Commit(this, refId);
}
//// Utility methods ////
/**
* Calculate the id, or hash of the given data.
*
* @param data the data
* @return the object id.
*/
public String hashData(byte[] data) {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("This Java implementation does not support SHA-1 hashing.");
}
md.update(data);
byte[] digest = md.digest();
String hexStr = "";
for (byte aDigest : digest) {
hexStr += Integer.toString((aDigest & 0xff) + 0x100, 16).substring(1);
}
return hexStr;
}
public String hashDirectoryRecursive(File directory) {
if (!directory.exists())
throw new AssertionError("Directory '" + directory + "' does not exist.");
if (!directory.isDirectory())
throw new AssertionError("Directory '" + directory + "' is not a directory.");
StringBuffer treeDataBuffer = new StringBuffer();
for (File f : directory.listFiles()) {
String type, id;
if (f.isDirectory()) {
id = hashDirectoryRecursive(f);
type = "tree";
} else {
id = hashObject(f, true);
type = "blob";
}
treeDataBuffer.append(type).append(" ").append(id).append("\t").append(f.getName()).append("\n");
}
// Remove the final "\n"
String treeData = treeDataBuffer.substring(treeDataBuffer.length() - 1);
return writeObject(treeData.getBytes());
}
/**
* Read in a file as a stream of bytes.
*
* @param file the file
* @return a bytestream with the data of the file.
*/
private byte[] readFile(File file) {
try {
FileInputStream in = new FileInputStream(file);
long fileSize = file.length();
// Check if the file is too large
// (not larger than maximum value of an integer, which is the maximum size for a byte array).
byte[] bytes = new byte[(int) fileSize];
int offset = 0;
int numRead = 0;
while (offset < bytes.length &&
(numRead = in.read(bytes, offset, bytes.length - offset)) >= 0) {
offset += numRead;
}
// Ensure all bytes have been read.
if (offset < bytes.length)
throw new RuntimeException("Could not completely read file " + file);
in.close();
return bytes;
} catch (IOException e) {
throw new RuntimeException("Could not read file " + file, e);
}
}
/**
* Write a byte array to a file.
*
* @param file the file
* @param data a bytestream with the data of the file.
* @param offset the offset in the data stream.
* @param length the length of the data stream.
*/
private void writeFile(File file, byte[] data, int offset, int length) {
try {
FileOutputStream out = new FileOutputStream(file);
out.write(data, offset, length);
out.close();
} catch (IOException e) {
throw new RuntimeException("Could not read file " + file, e);
}
}
}