[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/xUser5000/gitlet/master/gitlet/gitlet/Repository.java [Back]  [Original]

package gitlet;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import static gitlet.Utils.*;

/** Represents a gitlet repository.
 *
 *  @author xUser5000
 */
public class Repository {

    /** The current working directory. */
    private final File CWD;
    /** The .gitlet directory. */
    private final File GITLET_DIR;

    private final File COMMITS_DIR;
    private final File BLOBS_DIR;
    private final File BRANCHES_DIR;
    private final File HEAD_FILE;
    private final File STAGED_DIR;
    private final File ADDITION_DIR;
    private final File REMOVAL_DIR;
    private final File REMOTE_DIR;

    private final WorkingArea workingArea;
    private final CommitStore commitStore;
    private final BranchStore branchStore;
    private final StagingArea stagingArea;
    private final Head head;
    private final BlobStore blobStore;
    private final RemoteStore remoteStore;

    public Repository(String currentWorkingDirectory) {
        if (currentWorkingDirectory == null || currentWorkingDirectory.isBlank()) {
            throw new RuntimeException("Working directory not found.");
        }

        CWD = new File(currentWorkingDirectory);
        if (!CWD.exists()) {
            throw new RuntimeException("Working directory not found.");
        }

        GITLET_DIR = join(CWD, ".gitlet");
        COMMITS_DIR = join(GITLET_DIR, "commits");
        BLOBS_DIR = join(GITLET_DIR, "blobs");
        BRANCHES_DIR = join(GITLET_DIR, "branches");
        HEAD_FILE = join(GITLET_DIR, "HEAD");
        STAGED_DIR = join(GITLET_DIR, "staged");
        ADDITION_DIR = join(STAGED_DIR, "addition");
        REMOVAL_DIR = join(STAGED_DIR, "removal");
        REMOTE_DIR = join(GITLET_DIR, "remote");

        workingArea = new WorkingArea(CWD);
        commitStore = new CommitStore(COMMITS_DIR);
        branchStore = new BranchStore(BRANCHES_DIR);
        stagingArea = new StagingArea(ADDITION_DIR, REMOVAL_DIR);
        head = new Head(HEAD_FILE);
        blobStore = new BlobStore(BLOBS_DIR);
        remoteStore = new RemoteStore(REMOTE_DIR);
    }

    /**
     * Initialize the directory structure inside .gitlet
     * Start with a single branch called "master"
     * Start with one commit with the message "initial commit" with a timestamp = Unix epoch
     * Set HEAD to point to the master branch
     */
    public void init() throws IOException {
        if (GITLET_DIR.exists()) {
            exitWithMessage("A Gitlet version-control system already exists in the current directory.");
        }
        GITLET_DIR.mkdir();
        COMMITS_DIR.mkdir();
        BLOBS_DIR.mkdir();
        BRANCHES_DIR.mkdir();
        STAGED_DIR.mkdir();
        ADDITION_DIR.mkdir();
        REMOVAL_DIR.mkdir();
        HEAD_FILE.createNewFile();

        Commit initialCommit = new Commit.Builder("initial commit").timestamp(new Date(0)).build();
        commitStore.saveCommit(initialCommit);

        Branch masterBranch = new Branch("master", initialCommit.getHash());
        branchStore.saveBranch(masterBranch);

        setCurrentBranch(masterBranch);
    }

    /**
     * Add a file in the working directory to the staging area
     * If the file does not exist, print `File does not exist.`
     * If the current working version of the file is identical to the version in the current commit,
     *      do not stage it to be added, and remove it from the staging area if it is already there.
     * Staging an already-staged file overwrites the previous entry in the staging area with the new contents.
     * Stage the file for addition
     * If the file was staged for removal, it will no longer be after executing the command
     */
    public void add(String fileName) {
        checkInitializedGitletDirectory();
        File workingFile = workingArea.getFile(fileName);
        if (workingFile == null) {
            exitWithMessage("File does not exist.");
        }

        String committedFileHash = getCurrentCommit().getTrackedFiles().get(fileName);
        String workingFileHash = sha1(readContentsAsString(workingFile));
        if (!workingFileHash.equals(committedFileHash)) {
            stagingArea.stageForAddition(workingFile);
        } else {
            stagingArea.unstageForAddition(fileName);
        }

        stagingArea.unstageForRemoval(fileName);
    }

    /**
     * If the commit message is empty, print `Please enter a commit message.`
     * If no files have been staged, print `No changes added to the commit.`
     * For each added/modified files in the staging area,
     *      - Compute its SHA-1 hash as a function of its contents
     *      - Copy it to the blobs directory and set its to its SHA-1 hash
     * Build the commit
     *      - Reuse all tracked files from the parent commit
     *      - Rewire the references of the added/modified files
     *      - Untrack removed files
     *      - Point to the parent commit
     * Save the commit on disk
     * Update the pointer of the current branch to point to the new commit
     * Clear the staging area
     */
    public void commit(String message) {
        checkInitializedGitletDirectory();
        commit(message, null);
    }

    private void commit(String message, String secondaryParent) {
        if (message.isEmpty()) {
            exitWithMessage("Please enter a commit message.");
        }

        if (stagingArea.isEmpty()) {
            exitWithMessage("No changes added to the commit.");
        }

        /* Compute the SHA-1 hash of added/modified files and store them in the blobs directory */
        Map nameToBlob = new TreeMap();
        stagingArea.getFilesForAddition().forEach(file -> {
            File storedBlob = blobStore.save(file);
            nameToBlob.put(file.getName(), storedBlob.getName());
        });

        /* update the tracked list of files */
        Map trackedFiles = getCurrentCommit().getTrackedFiles();
        trackedFiles.putAll(nameToBlob);
        stagingArea.getFilesForRemoval().forEach(file -> trackedFiles.remove(file.getName()));

        Commit newCommit = new Commit.Builder(message)
                .parent(getCurrentCommit().getHash())
                .secondaryParent(secondaryParent)
                .trackedFiles(trackedFiles)
                .build();
        commitStore.saveCommit(newCommit);

        Branch branch = getCurrentBranch();
        branch.setCommit(newCommit.getHash());
        branchStore.saveBranch(branch);

        stagingArea.clear();
    }

    /**
     * If the file is neither staged nor tracked by the head commit, print `No reason to remove the file.`
     * If the file is currently staged for addition, unstage it
     * If the file is tracked in the current commit,
     *      - stage it for removal
     *      - remove it from the working directory if the user has not already done so
     */
    public void rm(String filename) {
        checkInitializedGitletDirectory();
        File stagedForAdditionFile = stagingArea.getFileForAddition(filename);
        Map trackedFiles = getCurrentCommit().getTrackedFiles();
        if (stagedForAdditionFile == null && !trackedFiles.containsKey(filename)) {
            exitWithMessage("No reason to remove the file.");
        }
        if (stagedForAdditionFile != null) {
            stagingArea.unstageForAddition(filename);
        }
        if (trackedFiles.containsKey(filename)) {
            File trackedFile = blobStore.get(trackedFiles.get(filename));
            stagingArea.stageForRemoval(readContentsAsString(trackedFile), filename);
            workingArea.deleteFile(filename);
        }
    }

    /**
     * Display information about each commit backwards along the commit tree
     *      starting from the commit in the current HEAD until the initial commit
     * More info in Commit::log()
     */
    public void log() {
        checkInitializedGitletDirectory();
        getCommitChain(getCurrentCommit()).stream()
                .map(Commit::log)
                .forEach(System.out::print);
    }

    /**
     * Like log, except displays information about all commits ever made.
     * The order of the commits does not matter
     */
    public void globalLog() {
        checkInitializedGitletDirectory();
        commitStore
                .allCommitsStream()
                .map(Commit::log)
                .forEach(System.out::print);
    }

    /**
     * Prints out the ids of all commits that have the given commit message, one per line
     * If no such commit exists, print `Found no commit with that message.`
     */
    public void find(String commitMessage) {
        checkInitializedGitletDirectory();
        List matchedCommits = commitStore.getCommitsByMessage(commitMessage);
        if (matchedCommits.isEmpty()) {
            exitWithMessage("Found no commit with that message.");
        }
        matchedCommits.stream()
                .map(Commit::getHash)
                .forEach(System.out::println);
    }

    /**
     * Displays what branches currently exist, and marks the current branch with a *
     * Displays what files have been staged for addition or removal
     * Entries are listed in lexicographic order
     * A file in the working directory is modified but not staged if it is
     *      - Tracked in the current commit, changed in the working directory, but not staged; or
     *      - Staged for addition, but with different contents than in the working directory; or
     *      - Staged for addition, but deleted in the working directory; or
     *      - Not staged for removal, but tracked in the current commit and deleted from the working directory.
     * Untracked Files is for files present in the working directory but neither staged for addition nor tracked
     *      - This includes files that have been staged for removal, but then re-created without Gitlets knowledge
     */
    public void status() {
        checkInitializedGitletDirectory();
        System.out.println("=== Branches ===");
        List branches = branchStore.allBranches();
        Branch currentBranch = getCurrentBranch();
        branches.forEach(branch -> {
            if (branch.getName().equals(currentBranch.getName())) {
                System.out.print("*");
            }
            System.out.println(branch.getName());
        });
        System.out.println();

        System.out.println("=== Staged Files ===");
        List addedFiles = stagingArea.getFilesForAddition();
        addedFiles.stream().map(File::getName).forEach(System.out::println);
        System.out.println();

        System.out.println("=== Removed Files ===");
        List removedFiles = stagingArea.getFilesForRemoval();
        removedFiles.stream().map(File::getName).forEach(System.out::println);
        System.out.println();

        System.out.println("=== Modifications Not Staged For Commit ===");
        // TODO: Modifications not staged for commit
        System.out.println();

        System.out.println("=== Untracked Files ===");
        // TODO: Untracked files
        System.out.println();
    }

    /**
     * Takes the version of the file as it exists in the commit with the given id, and puts it in the working directory,
     *      overwriting the version of the file thats already there if there is one.
     * If no commit with the given id exists, print `No commit with that id exists.`
     * If the file does not exist in the previous commit, print `File does not exist in that commit.`
     * The new version of the file is not staged.
     */
    public void checkoutFile(String commitHash, String fileName) {
        checkInitializedGitletDirectory();
        Commit commit = commitStore.getCommitByHash(commitHash);
        if (commit == null) {
            exitWithMessage("No commit with that id exists.");
        }

        String blobHash = commit.getTrackedFiles().get(fileName);
        if (blobHash == null) {
            exitWithMessage("File does not exist in that commit.");
        }

        File blob = blobStore.get(blobHash);
        String blobContents = readContentsAsString(blob);
        workingArea.saveFile(blobContents, fileName);
    }

    /**
     * same as checkoutFile(String commitHash, String fileName) except that commitHash is the current commit hash
     */
    public void checkoutFile(String fileName) {
        checkInitializedGitletDirectory();
        checkoutFile(getCurrentCommit().getHash(), fileName);
    }

    /**
     * Reset the current working directory to a given commit
     * If a working file is untracked in the current branch and would be overwritten by the checkout,
     *      print `There is an untracked file in the way; delete it, or add and commit it first.`
     * Clears the staging area
     */
    private void checkoutCommit(Commit targetCommit) {
        if (workingArea.allFiles().stream()
                .map(File::getName)
                .filter(fileName -> !getCurrentCommit().getTrackedFiles().containsKey(fileName))
                .anyMatch(fileName -> targetCommit.getTrackedFiles().containsKey(fileName))
        ) {
            exitWithMessage("There is an untracked file in the way; delete it, or add and commit it first.");
        }

        workingArea.clear();
        stagingArea.clear();

        targetCommit.getTrackedFiles().keySet().forEach(fileName -> checkoutFile(targetCommit.getHash(), fileName));
    }

    /**
     * Takes all files in the commit at the head of the given branch, and puts them in the working directory,
     *      overwriting the versions of the files that are already there if they exist
     * If that branch is the current branch, print `No need to checkout the current branch.`
     * If no branch with that name exists, print `No such branch exists.`
     * Any files that are tracked in the current branch but are not present in the checked-out branch are deleted
     * The staging area is cleared, unless the checked-out branch is the current branch
     * Given branch will now be considered the current branch (HEAD)
     */
    public void checkoutBranch(String targetBranchName) {
        checkInitializedGitletDirectory();
        Branch currentBranch = getCurrentBranch();
        if (targetBranchName.equals(currentBranch.getName())) {
            exitWithMessage("No need to checkout the current branch.");
        }

        Branch targetBranch = branchStore.getBranch(targetBranchName);
        if (targetBranch == null) {
            exitWithMessage("No such branch exists.");
        }

        Commit targetCommit = commitStore.getCommitByHash(targetBranch.getCommitHash());
        checkoutCommit(targetCommit);

        setCurrentBranch(targetBranch);
    }

    /**
     * Creates a new branch with the given name, and points it at the current head commit
     * This command does NOT immediately switch to the newly created branch (just as in real Git)
     * If a branch with the given name already exists, print the error message `A branch with that name already exists`
     */
    public void branch(String branchName) {
        checkInitializedGitletDirectory();
        if (branchStore.getBranch(branchName) != null) {
            exitWithMessage("A branch with that name already exists");
        }
        Branch branch = new Branch(branchName, getCurrentBranch().getCommitHash());
        branchStore.saveBranch(branch);
    }

    /**
     * Deletes the branch with the given name
     * If a branch with the given name does not exist, print `A branch with that name does not exist.`
     * If applied to the branch we are currently in, print `Cannot remove the current branch.`
     */
    public void rmBranch(String branchName) {
        checkInitializedGitletDirectory();
        if (getCurrentBranch().getName().equals(branchName)) {
            exitWithMessage("Cannot remove the current branch.");
        }
        Branch branch = branchStore.getBranch(branchName);
        if (branch == null) {
            exitWithMessage("A branch with that name does not exist.");
        }
        branchStore.removeBranch(branch);
    }

    /**
     * Checks out all the files tracked by the given commit
     *      The [commit id] may be abbreviated as for checkout
     * If no commit with the given id exists, print `No commit with that id exists.`
     * If a working file is untracked in the current branch and would be overwritten by the reset,
     *      print `There is an untracked file in the way; delete it, or add and commit it first.`
     * Remove tracked files that are not present in that commit
     * Clear the staging area
     * Move the current branchs head to that commit
     */
    public void reset(String commitHash) {
        checkInitializedGitletDirectory();
        Commit targetCommit = commitStore.getCommitByHash(commitHash);
        if (targetCommit == null) {
            exitWithMessage("No commit with that id exists.");
        }
        checkoutCommit(targetCommit);

        Branch currentBranch = getCurrentBranch();
        currentBranch.setCommit(targetCommit);
        branchStore.saveBranch(currentBranch);
    }

    /**
     * Merges files from the given branch into the current branch.
     * If the staging area is not empty, print `You have uncommitted changes.`
     * If a branch with the given name does not exist, print `A branch with that name does not exist.`
     * If curren branch == given branch, print `Cannot merge a branch with itself.`
     * If an untracked file in the current commit would be overwritten or deleted by the merge,
     *      print `There is an untracked file in the way; delete it, or add and commit it first.`
     * If the split point is the same commit as the given branch,
     *      - do nothing
     *      - print `Given branch is an ancestor of the current branch.`
     *      - merge is complete
     * If the split point is the current branch,
     *      - check out the given branch,
     *      - print `Current branch fast-forwarded.`
     *      - merge is complete
     * Merging rules (denoting the current branch as HEAD, the given branch as OTHER, and the split commit as SPLIT):
     *      1. If the file is modified in OTHER but not HEAD, use the version in OTHER
     *      2. If the file is modified in HEAD but not OTHER, use the version in HEAD
     *      3. If the file is modified in OTHER and HEAD,
     *          3.a. if HEAD and OTHER are modified in the same way, do nothing
     *          3.b. if HEAD and OTHER are modified in different ways, conflict
     *      4. If the file is not in SPLIT nor OTHER but in HEAD, use the version in HEAD
     *      5. If the file is not in SPLIT nor HEAD but in OTHER, use the version in OTHER
     *      6. If the file is unmodified in HEAD but not present in OTHER, remove
     *      7. If the file is unmodified in OTHER but not present in HEAD, remain removed
     * Whenever the resulting file of the merge is different from its version in HEAD,
     *      add it to the staging area
     * When a conflict occurs, fill the conflicted file with the following:
     * ```
     * >
     * ```
     * Once files have been updated according to the above rules,
     *      - commit with the message: `Merged [given branch name] into [current branch name].`
     *      - if the merge encountered a conflict, print the message `Encountered a merge conflict.`
     */
    public void merge(String branchName) {
        checkInitializedGitletDirectory();
        if (!stagingArea.isEmpty()) {
            exitWithMessage("You have uncommitted changes.");
        }

        Branch targetBranch = branchStore.getBranch(branchName);
        if (targetBranch == null) {
            exitWithMessage("A branch with that name does not exist.");
        }

        Branch currentBranch = getCurrentBranch();
        if (targetBranch.getName().equals(currentBranch.getName())) {
            exitWithMessage("Cannot merge a branch with itself.");
        }

        if (workingArea.allFiles().stream()
                .anyMatch(file -> !getCurrentCommit().getTrackedFiles().containsKey(file.getName()))
        ) {
            exitWithMessage("There is an untracked file in the way; delete it, or add and commit it first.");
        }

        final Commit HEAD_COMMIT = commitStore.getCommitByHash(currentBranch.getCommitHash());
        final Commit OTHER_COMMIT = commitStore.getCommitByHash(targetBranch.getCommitHash());
        final Commit SPLIT_COMMIT = splitPoint(currentBranch, targetBranch);

        if (SPLIT_COMMIT.equals(OTHER_COMMIT)) {
            exitWithMessage("Given branch is an ancestor of the current branch.");
        }

        if (SPLIT_COMMIT.equals(HEAD_COMMIT)) {
            checkoutBranch(targetBranch.getName());
            exitWithMessage("Current branch fast-forwarded.");
        }

        Set filePool = new HashSet();
        filePool.addAll(SPLIT_COMMIT.getTrackedFiles().keySet());
        filePool.addAll(HEAD_COMMIT.getTrackedFiles().keySet());
        filePool.addAll(OTHER_COMMIT.getTrackedFiles().keySet());

        AtomicBoolean isConflict = new AtomicBoolean(false);
        filePool.forEach(fileName -> {
            final String SPLIT = SPLIT_COMMIT.getTrackedFiles().get(fileName);
            final String HEAD = HEAD_COMMIT.getTrackedFiles().get(fileName);
            final String OTHER = OTHER_COMMIT.getTrackedFiles().get(fileName);

            if (SPLIT != null && OTHER != null && !SPLIT.equals(OTHER) && SPLIT.equals(HEAD)) {
                String contents = readContentsAsString(blobStore.get(OTHER));
                workingArea.saveFile(contents, fileName);
                stagingArea.stageForAddition(readContentsAsString(blobStore.get(OTHER)), fileName);
            }

            if (SPLIT != null && HEAD != null && !SPLIT.equals(HEAD) && SPLIT.equals(OTHER)) {
                workingArea.saveFile(readContentsAsString(blobStore.get(HEAD)), fileName);
            }

            if (
                    !Objects.equals(SPLIT, HEAD) && !Objects.equals(SPLIT, OTHER) && !Objects.equals(HEAD, OTHER)
            ) {
                /* conflict */
                String headContents = (HEAD != null ? readContentsAsString(blobStore.get(HEAD)) : "");
                String otherContents = (OTHER != null ? readContentsAsString(blobStore.get(OTHER)) : "");
                String contents = ">\n";
                workingArea.saveFile(contents, fileName);
                stagingArea.stageForAddition(contents, fileName);
                isConflict.set(true);
            }

            if (SPLIT == null && OTHER == null && HEAD != null) {
                workingArea.saveFile(readContentsAsString(blobStore.get(HEAD)), fileName);
            }

            if (SPLIT == null && HEAD == null && OTHER != null) {
                String contents = readContentsAsString(blobStore.get(OTHER));
                workingArea.saveFile(contents, fileName);
                stagingArea.stageForAddition(readContentsAsString(blobStore.get(OTHER)), fileName);
            }

            if (SPLIT != null && SPLIT.equals(HEAD) && OTHER == null) {
                stagingArea.stageForRemoval(workingArea.getFile(fileName));
                workingArea.deleteFile(fileName);
            }

            if (SPLIT != null && SPLIT.equals(OTHER) && HEAD == null) {
                /* leave the file removed */
            }
        });

        String commitMessage = String.format(
                "Merged %s into %s.",
                targetBranch.getName(),
                currentBranch.getName());
        commit(commitMessage, OTHER_COMMIT.getHash());

        if (isConflict.get()) {
            System.out.println("Encountered a merge conflict.");
        }
    }

    /**
     * Saves the given login information under the given remote name.
     * If a remote with the given name already exists, print `A remote with that name already exists.`
     */
    public void addRemote(String name, String URI) throws IOException {
        remoteStore.addRemote(name, URI);
    }

    /**
     * Remove information associated with the given remote name.
     * If a remote with the given name does not exist, print `A remote with that name does not exist.`
     */
    public void rmRemote(String name) {
        remoteStore.removeRemote(name);
    }

    /**
     * finds the latest common ancestor of two branches in linear time relative to the size of the commit tree
     */
    private Commit splitPoint(Branch a, Branch b) {
        Commit A = commitStore.getCommitByHash(a.getCommitHash());
        Commit B = commitStore.getCommitByHash(b.getCommitHash());
        Set set = getCommitTree(A).stream().map(Commit::getHash).collect(Collectors.toSet());
        return getCommitTree(B).stream()
                .filter(commit -> set.contains(commit.getHash()))
                .max(Comparator.comparing(Commit::getTimestamp))
                .orElse(null);
    }

    private Commit getCurrentCommit() {
        String commitHash = getCurrentBranch().getCommitHash();
        return commitStore.getCommitByHash(commitHash);
    }

    private List getCommitChain(Commit startingCommit) {
        List commits = new ArrayList();
        Commit currentCommit = startingCommit;
        while (currentCommit != null) {
            commits.add(currentCommit);
            currentCommit = commitStore.getCommitByHash(currentCommit.getParent());
        }
        return commits;
    }

    private List getCommitTree(Commit rootCommit) {
        List result = new ArrayList();
        DFS(rootCommit, new HashSet(), result);
        return result;
    }

    private void DFS(Commit node, Set visited, List list) {
        list.add(node);
        visited.add(node.getHash());

        String primaryParent = node.getParent();
        String secondaryParent = node.getSecondaryParent();

        if (primaryParent != null && !visited.contains(primaryParent)) {
            DFS(commitStore.getCommitByHash(primaryParent), visited, list);
        }

        if (secondaryParent != null && !visited.contains(secondaryParent)) {
            DFS(commitStore.getCommitByHash(secondaryParent), visited, list);
        }
    }

    private Branch getCurrentBranch() {
        return branchStore.getBranch(head.get());
    }

    private void setCurrentBranch(Branch branch) {
        head.set(branch);
    }

    private void checkInitializedGitletDirectory() {
        if (!GITLET_DIR.exists()) {
            exitWithMessage("Not in an initialized Gitlet directory.");
        }
    }
}

Web Proxy Viewer  |  New URL  |  Original Page