/**
 *  Copyright 2005-2015 Red Hat, Inc.
 *
 *  Red Hat licenses this file to you under the Apache License, version
 *  2.0 (the "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 *  implied.  See the License for the specific language governing
 *  permissions and limitations under the License.
 */
package io.fabric8.forge.rest.git;

import io.fabric8.forge.rest.git.dto.CommitDetail;
import io.fabric8.forge.rest.git.dto.CommitInfo;
import io.fabric8.forge.rest.git.dto.CommitTreeInfo;
import io.fabric8.forge.rest.git.dto.DiffInfo;
import io.fabric8.forge.rest.git.dto.FileDTO;
import io.fabric8.forge.rest.git.dto.StatusDTO;
import io.fabric8.forge.rest.utils.StopWatch;
import io.fabric8.project.support.GitUtils;
import io.fabric8.forge.rest.main.MD5Util;
import io.fabric8.forge.rest.main.ProjectFileSystem;
import io.fabric8.project.support.UserDetails;
import io.fabric8.utils.Files;
import io.fabric8.utils.IOHelpers;
import io.fabric8.utils.Strings;
import io.fabric8.utils.Systems;
import io.fabric8.utils.URLUtils;
import org.eclipse.jgit.api.AddCommand;
import org.eclipse.jgit.api.CheckoutCommand;
import org.eclipse.jgit.api.CommitCommand;
import org.eclipse.jgit.api.CreateBranchCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.ListBranchCommand;
import org.eclipse.jgit.api.PullCommand;
import org.eclipse.jgit.api.PushCommand;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.NoHeadException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import org.gitective.core.BlobUtils;
import org.gitective.core.CommitFinder;
import org.gitective.core.CommitUtils;
import org.gitective.core.PathFilterUtils;
import org.gitective.core.filter.commit.CommitLimitFilter;
import org.gitective.core.filter.commit.CommitListFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.Callable;

import static io.fabric8.project.support.GitUtils.configureCommand;
import static io.fabric8.project.support.GitUtils.disableSslCertificateChecks;
import static io.fabric8.project.support.GitUtils.doAddCommitAndPushFiles;

/**
 */
public class RepositoryResource {
    private static final transient Logger LOG = LoggerFactory.getLogger(RepositoryResource.class);

    protected static String gravatarUrl = Systems.getEnvVarOrSystemProperty("GRAVATAR_URL", "http://www.gravatar.com/avatar");

    private final File gitFolder;
    private final File basedir;
    private final UserDetails userDetails;
    private final String remoteRepository;
    private final GitLockManager lockManager;
    private final ProjectFileSystem projectFileSystem;
    private final String origin;
    private final String cloneUrl;
    private final String branch;
    private PersonIdent personIdent;
    private String message;
    private String objectId;

    public RepositoryResource(File basedir, File gitFolder, UserDetails userDetails, String origin, String branch, String remoteRepository, GitLockManager lockManager, ProjectFileSystem projectFileSystem, String cloneUrl, String objectId) throws IOException, GitAPIException {
        this.basedir = basedir;
        this.gitFolder = gitFolder;
        this.userDetails = userDetails;
        this.remoteRepository = remoteRepository;
        this.lockManager = lockManager;
        this.projectFileSystem = projectFileSystem;
        this.origin = origin;
        this.cloneUrl = cloneUrl;
        this.branch = branch;
        createPersonIdent();
    }

    protected static String getFilePattern(String path) {
        return trimLeadingSlash(path);
    }

    public static String trimLeadingSlash(String name) {
        if (name != null && name.startsWith("/")) {
            name = name.substring(1);
        }
        return name;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public void setObjectId(String objectId) {
        this.objectId = objectId;
    }

    public String getObjectId() {
        return objectId;
    }

    @GET
    @Path("content/{path:.*}")
    public Response fileDetails(final @PathParam("path") String path) throws Exception {
        return gitReadOperation(new GitOperation<Response>() {
            @Override
            public Response call(Git git, GitContext context) throws Exception {
                return doFileDetails(git, path);
            }
        });
    }

    protected Response doFileDetails(Git git, String path) {
        if (Strings.isNotBlank(objectId)) {
            Repository r = git.getRepository();
            String blobPath = trimLeadingSlash(path);
            String content = BlobUtils.getContent(r, objectId, blobPath);
            FileDTO answer = FileDTO.createFileDTO(blobPath, objectId, content);
            return Response.ok(answer).build();
        } else {
            final File file = getRelativeFile(path);
            if (LOG.isDebugEnabled()) {
                LOG.debug("reading file: " + file.getPath());
            }
            if (!file.exists() || file.isDirectory()) {
                List<FileDTO> answer = new ArrayList<>();
                if (file.exists()) {
                    File[] files = file.listFiles();
                    if (files != null) {
                        for (File child : files) {
                            FileDTO dto = createFileDTO(child, false);
                            if (dto != null) {
                                answer.add(dto);
                            }
                        }
                    }
                }
                return Response.ok(answer).build();
            } else {
                FileDTO answer = createFileDTO(file, true);
                return Response.ok(answer).build();
            }
        }
    }

    @GET
    @Path("raw/{path:.*}")
    public Response rawFile(final @PathParam("path") String path) throws Exception {
        return gitReadOperation(new GitOperation<Response>() {
            @Override
            public Response call(Git git, GitContext context) throws Exception {
                return doRawFile(path);
            }
        });
    }

    protected Response doRawFile(String path) throws IOException {
        final File file = getRelativeFile(path);
        if (LOG.isDebugEnabled()) {
            LOG.debug("reading file: " + file.getPath());
        }
        if (file.isDirectory()) {
            // TODO return a listing?
            Object directoryDto = null;
            return Response.ok(directoryDto).build();
        } else {
            byte[] data = Files.readBytes(file);
            return Response.ok(data).build();
        }
    }

    @POST
    @Path("content/{path:.*}")
    @Consumes("*/*")
    public Response postFile(@PathParam("path") String path, @QueryParam("message") String message, final InputStream body) throws Exception {
        return uploadFile(path, message, body);
    }

    @POST
    @Path("content/{path:.*}")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    public Response postFileForm(@PathParam("path") String path, @FormParam("message") String message, @FormParam("file") String body) throws Exception {
        byte[] bytes = body.getBytes();
        return uploadFile(path, message, new ByteArrayInputStream(bytes));
    }

    @GET
    @Path("diff/{objectId1}")
    public String diff(@PathParam("objectId1") String objectId) throws Exception {
        return diff(objectId, null, null);
    }

    @GET
    @Path("diff/{objectId1}/{objectId2}")
    public String diff(@PathParam("objectId1") String objectId, @PathParam("objectId2") String baseObjectId) throws Exception {
        return diff(objectId, baseObjectId, null);
    }

    @GET
    @Path("diff/{objectId1}/{objectId2}/{path:.*}")
    public String diff(final @PathParam("objectId1") String objectId, final @PathParam("objectId2") String baseObjectId, final @PathParam("path") String pathOrBlobPath) throws Exception {
        return gitReadOperation(new GitOperation<String>() {
            @Override
            public String call(Git git, GitContext context) throws Exception {
                return doDiff(git, objectId, baseObjectId, pathOrBlobPath);
            }
        });
    }

    protected String doDiff(Git git, String objectId, String baseObjectId, String pathOrBlobPath) throws IOException {
        Repository r = git.getRepository();
        String blobPath = trimLeadingSlash(pathOrBlobPath);

        RevCommit commit;
        if (Strings.isNotBlank(objectId)) {
            commit = CommitUtils.getCommit(r, objectId);
        } else {
            commit = CommitUtils.getHead(r);
        }
        RevCommit baseCommit = null;
        if (Strings.isNotBlank(baseObjectId) && !Objects.equals(baseObjectId, objectId)) {
            baseCommit = CommitUtils.getCommit(r, baseObjectId);
        }

        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        DiffFormatter formatter = createDiffFormatter(r, buffer);

        RevTree commitTree = commit.getTree();
        RevTree baseTree;
        if (baseCommit == null) {
            if (commit.getParentCount() > 0) {
                final RevWalk rw = new RevWalk(r);
                RevCommit parent = rw.parseCommit(commit.getParent(0).getId());
                rw.dispose();
                baseTree = parent.getTree();
            } else {
                // FIXME initial commit. no parent?!
                baseTree = commitTree;
            }
        } else {
            baseTree = baseCommit.getTree();
        }

        List<DiffEntry> diffEntries = formatter.scan(baseTree, commitTree);
        if (blobPath != null && blobPath.length() > 0) {
            for (DiffEntry diffEntry : diffEntries) {
                if (diffEntry.getNewPath().equalsIgnoreCase(blobPath)) {
                    formatter.format(diffEntry);
                    break;
                }
            }
        } else {
            formatter.format(diffEntries);
        }
        formatter.flush();
        return buffer.toString();
    }

    protected static DiffFormatter createDiffFormatter(Repository r, OutputStream buffer) {
        DiffFormatter formatter = new DiffFormatter(buffer);
        formatter.setRepository(r);
        formatter.setDiffComparator(RawTextComparator.DEFAULT);
        formatter.setDetectRenames(true);
        return formatter;
    }

    @GET
    @Path("commitInfo/{commitId}")
    public CommitInfo commitInfo(final @PathParam("commitId") String commitId) throws Exception {
        return gitReadOperation(new GitOperation<CommitInfo>() {
            @Override
            public CommitInfo call(Git git, GitContext context) throws Exception {
                return doCommitInfo(git, commitId);
            }
        });
    }

    @GET
    @Path("commitDetail/{commitId}")
    public CommitDetail commitDetail(final @PathParam("commitId") String commitId) throws Exception {
        return gitReadOperation(new GitOperation<CommitDetail>() {
            @Override
            public CommitDetail call(Git git, GitContext context) throws Exception {
                return doCommitDetail(git, commitId);
            }
        });
    }

    protected CommitDetail doCommitDetail(Git git, String commitId) throws IOException {
        RevCommit baseCommit = doGetCommit(git, commitId);
        if (baseCommit == null) {
            return null;
        } else {
            List<DiffInfo> diffs = new ArrayList<>();

            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            Repository r = git.getRepository();
            DiffFormatter formatter = createDiffFormatter(r, buffer);

            //git.diff().setNewTree()
            RevTree commitTree = baseCommit.getTree();
            RevTree baseTree = null;
            ObjectId parentId = null;
            if (baseCommit.getParentCount() > 0) {
                final RevWalk rw = new RevWalk(r);
                parentId = baseCommit.getParent(0).getId();
                RevCommit parent = rw.parseCommit(parentId);
                baseTree = parent.getTree();
                rw.dispose();

            } else {
                // FIXME initial commit. no parent?!
                baseTree = commitTree;
            }
            if (baseTree == null) {
                baseTree = baseCommit.getTree();
            }

            List<DiffEntry> diffEntries = formatter.scan(baseTree, commitTree);
            if (diffEntries.isEmpty()) {
                // lets try get the previous commit
                String previousCommit = commitId + "~1";
                ObjectId resolve = r.resolve(previousCommit);
                RevTree newTree = null;
                if (resolve != null) {
                    final RevWalk rw = new RevWalk(r);
                    RevCommit parent = rw.parseCommit(resolve);
                    newTree = parent.getTree();
                    rw.dispose();
                }
                if (baseTree == null || newTree == null || !Objects.equals(baseTree.getId(), newTree.getId())) {
                    diffEntries = formatter.scan(newTree, commitTree);
                }
            }
            for (DiffEntry diffEntry : diffEntries) {
                formatter.format(diffEntry);
                formatter.flush();
                String diff = buffer.toString();
                buffer.reset();
                DiffInfo diffInfo = createDiffInfo(diffEntry, diff);
                diffs.add(diffInfo);
            }
            CommitInfo commitInfo = createCommitInfo(baseCommit);
            return new CommitDetail(commitInfo, diffs);
        }

    }

    protected DiffInfo createDiffInfo(DiffEntry diffEntry, String diff) {
        return new DiffInfo(diffEntry.getChangeType(), diffEntry.getNewPath(), toInt(diffEntry.getNewMode()), diffEntry.getOldPath(), toInt(diffEntry.getOldMode()), diff);
    }

    protected static int toInt(FileMode fileMode) {
        return fileMode != null ? fileMode.getBits() : 0;
    }

    protected CommitInfo doCommitInfo(Git git, String commitId) {
        RevCommit commit = doGetCommit(git, commitId);
        if (commit == null) {
            return null;
        } else {
            return createCommitInfo(commit);
        }
    }

    protected static RevCommit doGetCommit(Git git, String commitId) {
        Repository repository = git.getRepository();
        return CommitUtils.getCommit(repository, commitId);
    }

    /**
     * Returns the file changes in a commit
     */
    @GET
    @Path("commitTree/{commitId}")
    public List<CommitTreeInfo> getCommitTree(final @PathParam("commitId") String commitId) throws Exception {
        return gitReadOperation(new GitOperation<List<CommitTreeInfo>>() {
            @Override
            public List<CommitTreeInfo> call(Git git, GitContext context) throws Exception {
                return doGetCommitTree(git, commitId);
            }
        });
    }

    protected List<CommitTreeInfo> doGetCommitTree(Git git, String commitId) {
        Repository repository = git.getRepository();
        List<CommitTreeInfo> list = new ArrayList<CommitTreeInfo>();
        RevCommit commit = CommitUtils.getCommit(repository, commitId);
        if (commit != null) {
            RevWalk rw = new RevWalk(repository);
            try {
                if (commit.getParentCount() == 0) {
                    TreeWalk treeWalk = new TreeWalk(repository);
                    treeWalk.reset();
                    treeWalk.setRecursive(true);
                    treeWalk.addTree(commit.getTree());
                    while (treeWalk.next()) {
                        String pathString = treeWalk.getPathString();
                        ObjectId objectId = treeWalk.getObjectId(0);
                        int rawMode = treeWalk.getRawMode(0);
                        list.add(new CommitTreeInfo(pathString, pathString, 0, rawMode, objectId.getName(), commit.getId().getName(),
                                DiffEntry.ChangeType.ADD));
                    }
                    treeWalk.close();
                } else {
                    RevCommit parent = rw.parseCommit(commit.getParent(0).getId());
                    DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
                    df.setRepository(repository);
                    df.setDiffComparator(RawTextComparator.DEFAULT);
                    df.setDetectRenames(true);
                    List<DiffEntry> diffs = df.scan(parent.getTree(), commit.getTree());
                    for (DiffEntry diff : diffs) {
                        String objectId = diff.getNewId().name();
                        if (diff.getChangeType().equals(DiffEntry.ChangeType.DELETE)) {
                            list.add(new CommitTreeInfo(diff.getOldPath(), diff.getOldPath(), 0, diff
                                    .getNewMode().getBits(), objectId, commit.getId().getName(), diff
                                    .getChangeType()));
                        } else if (diff.getChangeType().equals(DiffEntry.ChangeType.RENAME)) {
                            list.add(new CommitTreeInfo(diff.getOldPath(), diff.getNewPath(), 0, diff
                                    .getNewMode().getBits(), objectId, commit.getId().getName(), diff
                                    .getChangeType()));
                        } else {
                            list.add(new CommitTreeInfo(diff.getNewPath(), diff.getNewPath(), 0, diff
                                    .getNewMode().getBits(), objectId, commit.getId().getName(), diff
                                    .getChangeType()));
                        }
                    }
                }
            } catch (Throwable e) {
                LOG.warn("Failed to walk tree for commit " + commitId + ". " + e, e);
            } finally {
                rw.dispose();
            }
        }
        return list;
    }

    @GET
    @Path("history")
    public List<CommitInfo> history(@QueryParam("limit") int limit) throws Exception {
        return history(null, null, limit);
    }

    @GET
    @Path("history/{commitId}/{path:.*}")
    public List<CommitInfo> history(@PathParam("commitId") final String objectId, @PathParam("path") final String pathOrBlobPath, @QueryParam("limit") final int limit) throws Exception {
        return gitReadOperation(new GitOperation<List<CommitInfo>>() {
            @Override
            public List<CommitInfo> call(Git git, GitContext context) throws Exception {
                return doHistory(git, objectId, pathOrBlobPath, limit);
            }
        });
    }

    protected List<CommitInfo> doHistory(Git git, String objectId, String pathOrBlobPath, int limit) {
        List<CommitInfo> results = new ArrayList<CommitInfo>();
        Repository r = git.getRepository();

        try {
            String head = getHEAD(git);
        } catch (Exception e) {
            LOG.error("Cannot find HEAD of this git repository! " + e, e);
            return results;
        }

        String path = trimLeadingSlash(pathOrBlobPath);

        CommitFinder finder = new CommitFinder(r);
        CommitListFilter filter = new CommitListFilter();
        if (Strings.isNotBlank(path)) {
            finder.setFilter(PathFilterUtils.and(path));
        }
        finder.setFilter(filter);

        if (limit > 0) {
            finder.setFilter(new CommitLimitFilter(limit).setStop(true));
        }
        if (Strings.isNotBlank(objectId)) {
            finder.findFrom(objectId);
        } else {
            if (Strings.isNotBlank(branch)) {
                ObjectId branchObjectId = getBranchObjectId(git);
                if (branchObjectId != null) {
                    finder = finder.findFrom(branchObjectId);
                } else {
                    finder = finder.findInBranches();
                }

            } else {
                finder.find();
            }
        }
        List<RevCommit> commits = filter.getCommits();
        for (RevCommit entry : commits) {
            CommitInfo commitInfo = createCommitInfo(entry);
            results.add(commitInfo);
        }
        return results;
    }

    @POST
    @Path("mkdir/{path:.*}")
    public CommitInfo createDirectory(@PathParam("commitId") final String path) throws Exception {
        return gitWriteOperation(new GitOperation<CommitInfo>() {
            @Override
            public CommitInfo call(Git git, GitContext context) throws Exception {
                return doCreateDirectory(git, path);
            }
        });
    }

    protected CommitInfo doCreateDirectory(Git git, String path) throws Exception {
        File file = getRelativeFile(path);
        if (file.exists()) {
            return null;
        }
        file.mkdirs();
        String filePattern = getFilePattern(path);
        AddCommand add = git.add().addFilepattern(filePattern).addFilepattern(".");
        add.call();

        CommitCommand commit = git.commit().setAll(true).setAuthor(personIdent).setMessage(message);
        RevCommit revCommit = commitThenPush(git, commit);
        return createCommitInfo(revCommit);
    }

    @POST
    @Path("revert/{commitId}/{path:.*}")
    public CommitInfo revert(@PathParam("commitId") final String objectId, @PathParam("path") final String blobPath) throws Exception {
        return gitWriteOperation(new GitOperation<CommitInfo>() {
            @Override
            public CommitInfo call(Git git, GitContext context) throws Exception {
                return doRevert(git, objectId, blobPath);
            }
        });
    }

    protected CommitInfo doRevert(Git git, String objectId, String blobPath) throws Exception {
        String contents = doGetContent(git, objectId, blobPath);
        if (contents != null) {
            return doWrite(git, blobPath, contents.getBytes(), personIdent, message);
        } else {
            return null;
        }
    }

    @POST
    @Path("mv/{path:.*}")
    public CommitInfo rename(@QueryParam("old") final String oldPath, @PathParam("path") final String newPath) throws Exception {
        return gitWriteOperation(new GitOperation<CommitInfo>() {
            @Override
            public CommitInfo call(Git git, GitContext context) throws Exception {
                return doRename(git, oldPath, newPath);
            }
        });
    }

    protected CommitInfo doRename(Git git, String oldPath, String newPath) throws Exception {
        File file = getRelativeFile(oldPath);
        File newFile = getRelativeFile(newPath);
        if (file.exists()) {
            File parentFile = newFile.getParentFile();
            parentFile.mkdirs();
            if (!parentFile.exists()) {
                throw new IOException("Could not create directory " + parentFile + " when trying to move " + file + " to " + newFile + ". Maybe a file permission issue?");
            }
            file.renameTo(newFile);
            String filePattern = getFilePattern(newPath);
            git.add().addFilepattern(filePattern).call();
            CommitCommand commit = git.commit().setAll(true).setAuthor(personIdent).setMessage(message);
            return createCommitInfo(commitThenPush(git, commit));
        } else {
            return null;
        }
    }

    @POST
    @Path("rm")
    @Consumes({"application/xml", "application/json", "text/json"})
    public CommitInfo remove(final List<String> paths) throws Exception {
        return gitWriteOperation(new GitOperation<CommitInfo>() {
            @Override
            public CommitInfo call(Git git, GitContext context) throws Exception {
                return doRemove(git, paths);
            }
        });
    }

    protected CommitInfo doRemove(Git git, List<String> paths) throws Exception {
        if (paths != null && paths.size() > 0) {
            int count = 0;
            for (String path : paths) {
                File file = getRelativeFile(path);
                if (file.exists()) {
                    count++;
                    Files.recursiveDelete(file);
                    String filePattern = getFilePattern(path);
                    git.rm().addFilepattern(filePattern).call();
                }
            }
            if (count > 0) {
                CommitCommand commit = git.commit().setAll(true).setAuthor(personIdent).setMessage(message);
                return createCommitInfo(commitThenPush(git, commit));
            }
        }
        return null;
    }

    @POST
    @Path("rm/{path:.*}")
    public CommitInfo remove(@PathParam("path") final String path) throws Exception {
        return gitWriteOperation(new GitOperation<CommitInfo>() {
            @Override
            public CommitInfo call(Git git, GitContext context) throws Exception {
                return doRemove(git, path);
            }
        });
    }

    @POST
    @Path("removeProject")
    public Response remove() throws Exception {
        return lockManager.withLock(gitFolder, new Callable<Response>() {

            @Override
            public Response call() throws Exception {
                LOG.info("Removing clone of project at " + basedir);
                Files.recursiveDelete(basedir);
                return Response.ok(new StatusDTO(basedir.getName(), "remove project")).build();
            }
        });
    }

    protected CommitInfo doRemove(Git git, String path) throws Exception {
        File file = getRelativeFile(path);
        if (file.exists()) {
            Files.recursiveDelete(file);
            String filePattern = getFilePattern(path);
            git.rm().addFilepattern(filePattern).call();
            CommitCommand commit = git.commit().setAll(true).setAuthor(personIdent).setMessage(message);
            return createCommitInfo(commitThenPush(git, commit));
        } else {
            return null;
        }
    }

    @GET
    @Path("listBranches")
    public List<String> listBranches() throws Exception {
        return gitReadOperation(new GitOperation<List<String>>() {
            @Override
            public List<String> call(Git git, GitContext context) throws Exception {
                return doListBranches(git);
            }
        });
    }

    protected List<String> doListBranches(Git git) throws Exception {
        SortedSet<String> names = new TreeSet<String>();
        List<Ref> call = git.branchList().setListMode(ListBranchCommand.ListMode.ALL).call();
        for (Ref ref : call) {
            String name = ref.getName();
            int idx = name.lastIndexOf('/');
            if (idx >= 0) {
                name = name.substring(idx + 1);
            }
            if (name.length() > 0) {
                names.add(name);
            }
        }
        return new ArrayList<String>(names);
    }

    public <T> T gitReadOperation(GitOperation<T> operation) throws Exception {
        return gitReadOperation(operation, new GitContext());
    }

    public <T> T gitReadOperation(GitOperation<T> operation, GitContext context) throws Exception {
        context.setRequireCommit(false);
        context.setRequirePush(false);
        return gitOperation(context, operation);
    }

    public <T> T gitWriteOperation(GitOperation<T> operation) throws Exception {
        return gitWriteOperation(operation, new GitContext());
    }

    public <T> T gitWriteOperation(GitOperation<T> operation, GitContext context) throws Exception {
        context.setRequireCommit(true);
        context.setRequirePush(true);
        return gitOperation(context, operation);
    }

    protected <T> T gitOperation(final GitContext context, final GitOperation<T> operation) throws Exception {
        return lockManager.withLock(gitFolder, new Callable<T>() {

            @Override
            public T call() throws Exception {
                StopWatch watch = new StopWatch();

                projectFileSystem.cloneRepoIfNotExist(userDetails, basedir, cloneUrl);

                FileRepositoryBuilder builder = new FileRepositoryBuilder();
                Repository repository = builder.setGitDir(gitFolder)
                        .readEnvironment() // scan environment GIT_* variables
                        .findGitDir() // scan up the file system tree
                        .build();

                Git git = new Git(repository);
                if (Strings.isNullOrBlank(origin)) {
                    throw new IOException("Could not find remote git URL for folder " + gitFolder.getPath());
                }

                CredentialsProvider credentials = userDetails.createCredentialsProvider();
                createPersonIdent();

                disableSslCertificateChecks();
                LOG.info("Stashing local changes to the repo");
                boolean hasHead = true;
                try {
                    git.log().all().call();
                    hasHead = git.getRepository().getAllRefs().containsKey("HEAD");
                } catch (NoHeadException e) {
                    hasHead = false;
                }
                if (hasHead) {
                    // lets stash any local changes just in case..
                    try {
                        git.stashCreate().setPerson(personIdent).setWorkingDirectoryMessage("Stash before a write").setRef("HEAD").call();
                    } catch (Throwable e) {
                        LOG.error("Failed to stash changes: " + e, e);
                        Throwable cause = e.getCause();
                        if (cause != null && cause != e) {
                            LOG.error("Cause: " + cause, cause);
                        }
                    }
                }

                checkoutBranch(git, context);
                if (context.isRequirePull()) {
                    doPull(git, context);
                }

                T result = operation.call(git, context);

                if (Strings.isNullOrBlank(message)) {
                    message = "";
                }
                if (context.isRequireCommit() && hasGitChanges(git)) {
                    doAddCommitAndPushFiles(git, userDetails, personIdent, branch, origin, message, isPushOnCommit());
                }

                LOG.info("Git operation took " + watch.taken());

                return result;
            }

        });
    }

    protected boolean hasGitChanges(Git git) throws GitAPIException {
        Status status = git.status().call();
        return anySetsNotEmpty(status.getAdded(), status.getChanged(), status.getModified(), status.getRemoved(), status.getUntracked());
    }

    protected boolean anySetsNotEmpty(Set<String>... sets) {
        for (Set<String> set : sets) {
            if (!set.isEmpty()) {
                return true;
            }
        }
        return false;
    }

    protected void createPersonIdent() {
        String user = userDetails.getUser();
        String authorEmail = userDetails.getEmail();
        this.personIdent = new PersonIdent(user, authorEmail);
    }

    protected void doPull(Git git, GitContext context) throws GitAPIException {
        StopWatch watch = new StopWatch();

        LOG.info("Performing a pull in git repository " + this.gitFolder + " on remote URL: " + this.remoteRepository);
        CredentialsProvider cp = userDetails.createCredentialsProvider();
        PullCommand command = git.pull();
        configureCommand(command, userDetails);
        command.setCredentialsProvider(cp).setRebase(true).call();
        LOG.info("Took " + watch.taken() + " to complete pull in git repository " + this.gitFolder + " on remote URL: " + this.remoteRepository);
    }

    protected Response uploadFile(final String path, final String message, final InputStream body) throws Exception {
        return gitWriteOperation(new GitOperation<Response>() {
            @Override
            public Response call(Git git, GitContext context) throws Exception {
                return doUploadFile(path, message, body);
            }
        });
    }

    protected Response doUploadFile(final String path, String message, final InputStream body) throws Exception {
        this.message = message;
        final File file = getRelativeFile(path);

        boolean exists = file.exists();
        if (LOG.isDebugEnabled()) {
            LOG.debug("writing file: " + file.getPath());
        }
        file.getParentFile().mkdirs();
        IOHelpers.writeTo(file, body);
        String status = exists ? "updated" : "created";
        return Response.ok(new StatusDTO(path, status)).build();
    }

    protected CommitInfo doWrite(Git git, String path, byte[] contents, PersonIdent personIdent, String commitMessage) throws Exception {
        File file = getRelativeFile(path);
        file.getParentFile().mkdirs();

        Files.writeToFile(file, contents);

        String filePattern = getFilePattern(path);
        AddCommand add = git.add().addFilepattern(filePattern).addFilepattern(".");
        add.call();

        CommitCommand commit = git.commit().setAll(true).setAuthor(personIdent).setMessage(commitMessage);
        RevCommit revCommit = commitThenPush(git, commit);
        return createCommitInfo(revCommit);
    }

    protected RevCommit commitThenPush(Git git, CommitCommand commit) throws Exception {
        RevCommit answer = commit.call();
        if (LOG.isDebugEnabled()) {
            LOG.debug("Committed " + answer.getId() + " " + answer.getFullMessage());
        }
        if (isPushOnCommit()) {
            Iterable<PushResult> results = doPush(git);
            for (PushResult result : results) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Pushed " + result.getMessages() + " " + result.getURI() + " branch: " + branch + " updates: " + toString(result.getRemoteUpdates()));
                }
            }
        }
        return answer;
    }

    protected File getRelativeFile(String path) {
        return new File(basedir, trimLeadingSlash(path));
    }

    protected boolean isPushOnCommit() {
        return true;
    }

    public CommitInfo createCommitInfo(RevCommit entry) {
        final Date date = GitUtils.getCommitDate(entry);
        PersonIdent authorIdent = entry.getAuthorIdent();
        String author = null;
        String name = null;
        String email = null;
        String avatarUrl = null;
        if (authorIdent != null) {
            author = authorIdent.getName();
            name = authorIdent.getName();
            email = authorIdent.getEmailAddress();

            // lets try default the avatar
            if (Strings.isNotBlank(email)) {
                avatarUrl = getAvatarUrl(email);
            }
        }
        boolean merge = entry.getParentCount() > 1;
        String shortMessage = entry.getShortMessage();
        String sha = entry.getName();
        return new CommitInfo(sha, author, name, email, avatarUrl, date, merge, shortMessage);
    }

    protected String getAvatarUrl(String email) {
        String hash = MD5Util.md5Hex(email);
        if (Strings.isNotBlank(hash)) {
            return URLUtils.pathJoin(gravatarUrl, hash);
        }
        return null;
    }

    protected String getHEAD(Git git) {
        RevCommit commit = CommitUtils.getHead(git.getRepository());
        return commit.getName();
    }

    protected ObjectId getBranchObjectId(Git git) {
        Ref branchRef = null;
        try {
            String branchRevName = "refs/heads/" + branch;
            List<Ref> branches = git.branchList().call();
            for (Ref ref : branches) {
                String revName = ref.getName();
                if (Objects.equals(branchRevName, revName)) {
                    branchRef = ref;
                    break;
                }
            }
        } catch (GitAPIException e) {
            LOG.warn("Failed to find branches " + e, e);
        }

        ObjectId branchObjectId = null;
        if (branchRef != null) {
            branchObjectId = branchRef.getObjectId();
        }
        return branchObjectId;
    }

    public String currentBranch(Git git) {
        try {
            return git.getRepository().getBranch();
        } catch (IOException e) {
            LOG.warn("Failed to get the current branch due: " + e.getMessage() + ". This exception is ignored.", e);
            return null;
        }
    }

    protected void checkoutBranch(Git git, GitContext context) throws GitAPIException {
        String current = currentBranch(git);
        if (Objects.equals(current, branch)) {
            return;
        }
        System.out.println("Checking out branch: " + branch);
        // lets check if the branch exists
        CheckoutCommand command = git.checkout().setName(branch);
        boolean exists = localBranchExists(git, branch);
        if (!exists) {
            command = command.setCreateBranch(true).setForce(true).
                    setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.TRACK).
                    setStartPoint(getRemote() + "/" + branch);
        }
        Ref ref = command.call();
        if (LOG.isDebugEnabled()) {
            LOG.debug("Checked out branch " + branch + " with results " + ref.getName());
        }
        configureBranch(git, branch);
    }


    protected String doGetContent(Git git, String objectId, String pathOrBlobPath) {
        objectId = defaultObjectId(git, objectId);
        Repository r = git.getRepository();
        String blobPath = trimLeadingSlash(pathOrBlobPath);
        return BlobUtils.getContent(r, objectId, blobPath);
    }

    protected String defaultObjectId(Git git, String objectId) {
        if (objectId == null || objectId.trim().length() == 0) {
            RevCommit commit = CommitUtils.getHead(git.getRepository());
            objectId = commit.getName();
        }
        return objectId;
    }

    protected void configureBranch(Git git, String branch) {
        // lets update the merge config
        if (Strings.isNotBlank(branch)) {
            StoredConfig config = git.getRepository().getConfig();
            if (Strings.isNullOrBlank(config.getString("branch", branch, "remote")) || Strings.isNullOrBlank(config.getString("branch", branch, "merge"))) {
                config.setString("branch", branch, "remote", getRemote());
                config.setString("branch", branch, "merge", "refs/heads/" + branch);
                try {
                    config.save();
                } catch (IOException e) {
                    LOG.error("Failed to save the git configuration to " + basedir
                            + " with branch " + branch + " on remote repo: " + remoteRepository + " due: " + e.getMessage() + ". This exception is ignored.", e);
                }
            }
        }
    }

    protected boolean localBranchExists(Git git, String branch) throws GitAPIException {
        List<Ref> list = git.branchList().call();
        String fullName = "refs/heads/" + branch;
        boolean localBranchExists = false;
        for (Ref ref : list) {
            String name = ref.getName();
            if (Objects.equals(name, fullName)) {
                localBranchExists = true;
            }
        }
        return localBranchExists;
    }

    protected String getRemote() {
        return origin;
    }

    protected FileDTO createFileDTO(File file, boolean includeContent) {
        File parentFile = file.getParentFile();
        String relativePath = null;
        try {
            relativePath = trimLeadingSlash(Files.getRelativePath(basedir, parentFile));
        } catch (IOException e) {
            LOG.warn("Failed to find relative path of " + parentFile.getPath() + ". " + e, e);
        }
        FileDTO answer = FileDTO.createFileDTO(file, relativePath, includeContent, "", false);
        String path = answer.getPath();
        if (path.equals(".git")) {
            // lets ignore the git folder!
            return null;
        }
        // TODO use the path to generate the links...
        // TODO generate the SHA
        return answer;
    }

    public static boolean includeFile(File file, String relativePath) {
        return true;
    }


    protected String toString(Collection<RemoteRefUpdate> updates) {
        StringBuilder builder = new StringBuilder();
        for (RemoteRefUpdate update : updates) {
            if (builder.length() > 0) {
                builder.append(" ");
            }
            builder.append(update.getMessage() + " " + update.getRemoteName() + " " + update.getNewObjectId());
        }
        return builder.toString();
    }

    protected Iterable<PushResult> doPush(Git git) throws Exception {
        PushCommand command = git.push();
        configureCommand(command, userDetails);
        return command.setRemote(getRemote()).call();
    }

    public String getCloneUrl() {
        return cloneUrl;
    }
}