Java tutorial
/** * Copyright 2014 Maurcio Aniche * Licensed 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 br.com.metricminer2.scm; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import org.apache.log4j.Logger; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.CannotDeleteCurrentBranchException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.NotMergedException; import org.eclipse.jgit.blame.BlameResult; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry.ChangeType; import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.RawTextComparator; import org.eclipse.jgit.errors.AmbiguousObjectException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevSort; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.EmptyTreeIterator; import org.eclipse.jgit.util.io.DisabledOutputStream; import br.com.metricminer2.domain.ChangeSet; import br.com.metricminer2.domain.Commit; import br.com.metricminer2.domain.Developer; import br.com.metricminer2.domain.ModificationType; import br.com.metricminer2.util.FileUtils; public class GitRepository implements SCM { private static final int MAX_SIZE_OF_A_DIFF = 100000; private static final int MAX_NUMBER_OF_FILES_IN_A_COMMIT = 50; private String path; private static Logger log = Logger.getLogger(GitRepository.class); public GitRepository(String path) { this.path = path; } public static SCMRepository singleProject(String path) { return new GitRepository(path).info(); } public static SCMRepository[] allProjectsIn(String path) { List<SCMRepository> repos = new ArrayList<SCMRepository>(); for (String dir : FileUtils.getAllDirsIn(path)) { repos.add(singleProject(dir)); } return repos.toArray(new SCMRepository[repos.size()]); } public SCMRepository info() { RevWalk rw = null; Git git = null; try { git = Git.open(new File(path)); AnyObjectId headId = git.getRepository().resolve(Constants.HEAD); rw = new RevWalk(git.getRepository()); RevCommit root = rw.parseCommit(headId); rw.sort(RevSort.REVERSE); rw.markStart(root); RevCommit lastCommit = rw.next(); String origin = git.getRepository().getConfig().getString("remote", "origin", "url"); return new SCMRepository(this, origin, path, headId.getName(), lastCommit.getName()); } catch (Exception e) { throw new RuntimeException("error when info " + path, e); } finally { if (rw != null) rw.release(); if (git != null) git.close(); } } public ChangeSet getHead() { Git git = null; try { git = Git.open(new File(path)); ObjectId head = git.getRepository().resolve(Constants.HEAD); RevWalk revWalk = new RevWalk(git.getRepository()); RevCommit r = revWalk.parseCommit(head); return new ChangeSet(r.getName(), convertToDate(r)); } catch (Exception e) { throw new RuntimeException("error in getHead() for " + path, e); } finally { if (git != null) git.close(); } } @Override public List<ChangeSet> getChangeSets() { Git git = null; try { git = Git.open(new File(path)); List<ChangeSet> allCs = new ArrayList<ChangeSet>(); for (RevCommit r : git.log().all().call()) { String hash = r.getName(); GregorianCalendar date = convertToDate(r); allCs.add(new ChangeSet(hash, date)); } return allCs; } catch (Exception e) { throw new RuntimeException("error in getChangeSets for " + path, e); } finally { if (git != null) git.close(); } } private GregorianCalendar convertToDate(RevCommit revCommit) { GregorianCalendar date = new GregorianCalendar(); date.setTime(new Date(revCommit.getCommitTime() * 1000L)); return date; } @Override public Commit getCommit(String id) { Git git = null; try { git = Git.open(new File(path)); Repository repo = git.getRepository(); Iterable<RevCommit> commits = git.log().add(repo.resolve(id)).call(); Commit theCommit = null; for (RevCommit jgitCommit : commits) { Developer author = new Developer(jgitCommit.getAuthorIdent().getName(), jgitCommit.getAuthorIdent().getEmailAddress()); Developer committer = new Developer(jgitCommit.getCommitterIdent().getName(), jgitCommit.getCommitterIdent().getEmailAddress()); String msg = jgitCommit.getFullMessage().trim(); String hash = jgitCommit.getName().toString(); long epoch = jgitCommit.getCommitTime(); String parent = (jgitCommit.getParentCount() > 0) ? jgitCommit.getParent(0).getName().toString() : ""; GregorianCalendar date = new GregorianCalendar(); date.setTime(new Date(epoch * 1000L)); theCommit = new Commit(hash, author, committer, date, msg, parent); List<DiffEntry> diffsForTheCommit = diffsForTheCommit(repo, jgitCommit); if (diffsForTheCommit.size() > MAX_NUMBER_OF_FILES_IN_A_COMMIT) { log.error("commit " + id + " has more than files than the limit"); throw new RuntimeException("commit " + id + " too big, sorry"); } for (DiffEntry diff : diffsForTheCommit) { ModificationType change = Enum.valueOf(ModificationType.class, diff.getChangeType().toString()); String oldPath = diff.getOldPath(); String newPath = diff.getNewPath(); String diffText = ""; String sc = ""; if (diff.getChangeType() != ChangeType.DELETE) { diffText = getDiffText(repo, diff); sc = getSourceCode(repo, diff); } if (diffText.length() > MAX_SIZE_OF_A_DIFF) { log.error("diff for " + newPath + " too big"); diffText = "-- TOO BIG --"; } theCommit.addModification(oldPath, newPath, change, diffText, sc); } break; } return theCommit; } catch (Exception e) { throw new RuntimeException("error detailing " + id + " in " + path, e); } finally { if (git != null) git.close(); } } private List<DiffEntry> diffsForTheCommit(Repository repo, RevCommit commit) throws IOException, AmbiguousObjectException, IncorrectObjectTypeException { AnyObjectId currentCommit = repo.resolve(commit.getName()); AnyObjectId parentCommit = commit.getParentCount() > 0 ? repo.resolve(commit.getParent(0).getName()) : null; DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE); df.setBinaryFileThreshold(2 * 1024); // 2 mb max a file df.setRepository(repo); df.setDiffComparator(RawTextComparator.DEFAULT); df.setDetectRenames(true); List<DiffEntry> diffs = null; if (parentCommit == null) { RevWalk rw = new RevWalk(repo); diffs = df.scan(new EmptyTreeIterator(), new CanonicalTreeParser(null, rw.getObjectReader(), commit.getTree())); rw.release(); } else { diffs = df.scan(parentCommit, currentCommit); } df.release(); return diffs; } private String getSourceCode(Repository repo, DiffEntry diff) throws MissingObjectException, IOException, UnsupportedEncodingException { try { ObjectReader reader = repo.newObjectReader(); byte[] bytes = reader.open(diff.getNewId().toObjectId()).getBytes(); return new String(bytes, "utf-8"); } catch (Throwable e) { return ""; } } private String getDiffText(Repository repo, DiffEntry diff) throws IOException, UnsupportedEncodingException { DiffFormatter df2 = null; try { String diffText; ByteArrayOutputStream out = new ByteArrayOutputStream(); df2 = new DiffFormatter(out); df2.setRepository(repo); df2.format(diff); diffText = out.toString("UTF-8"); return diffText; } catch (Throwable e) { return ""; } finally { if (df2 != null) df2.release(); } } public void checkout(String hash) { Git git = null; try { git = Git.open(new File(path)); deleteMMBranch(git); git.checkout().setCreateBranch(true).setName("mm").setStartPoint(hash).setForce(true).call(); } catch (Exception e) { throw new RuntimeException(e); } finally { if (git != null) git.close(); } } private void deleteMMBranch(Git git) throws GitAPIException, NotMergedException, CannotDeleteCurrentBranchException { List<Ref> refs = git.branchList().call(); for (Ref r : refs) { if (r.getName().endsWith("mm")) { git.branchDelete().setBranchNames("mm").setForce(true).call(); break; } } } public List<RepositoryFile> files() { List<RepositoryFile> all = new ArrayList<RepositoryFile>(); for (File f : getAllFilesInPath()) { if (isNotAnImportantFile(f)) continue; all.add(new RepositoryFile(f)); } return all; } private boolean isNotAnImportantFile(File f) { return f.getName().equals(".DS_Store"); } public void reset() { Git git = null; try { git = Git.open(new File(path)); git.checkout().setName("master").setForce(true).call(); git.branchDelete().setBranchNames("mm").setForce(true).call(); } catch (Exception e) { throw new RuntimeException(e); } finally { if (git != null) git.close(); } } private List<File> getAllFilesInPath() { return FileUtils.getAllFilesInPath(path, new ArrayList<File>()); } @Override public long totalCommits() { return getChangeSets().size(); } @Override public String blame(String file, String currentCommit, Integer line) { Git git = null; try { git = Git.open(new File(path)); Iterable<RevCommit> commits = git.log().add(git.getRepository().resolve(currentCommit)).call(); ObjectId prior = commits.iterator().next().getParent(0).getId(); BlameResult blameResult = git.blame().setFilePath(file).setStartCommit(prior).setFollowFileRenames(true) .call(); return blameResult.getSourceCommit(line).getId().getName(); } catch (Exception e) { throw new RuntimeException(e); } finally { if (git != null) git.close(); } } }