package org.koshinuke.logic; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.file.DirectoryStream; import java.nio.file.Path; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.iharder.Base64; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.blame.BlameResult; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.RawText; import org.eclipse.jgit.diff.RawTextComparator; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.ObjectStream; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.AndTreeFilter; import org.eclipse.jgit.treewalk.filter.PathFilter; import org.eclipse.jgit.treewalk.filter.PathFilterGroup; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.StringUtils; import org.koshinuke._; import org.koshinuke.conf.Configuration; import org.koshinuke.jgit.SimpleDiffFormatter; import org.koshinuke.model.BlameModel; import org.koshinuke.model.BlobModel; import org.koshinuke.model.BranchHistoryModel; import org.koshinuke.model.CommitModel; import org.koshinuke.model.DiffEntryModel; import org.koshinuke.model.DiffModel; import org.koshinuke.model.KoshinukePrincipal; import org.koshinuke.model.NodeModel; import org.koshinuke.model.RepositoryModel; import org.koshinuke.util.GitUtil; import org.koshinuke.util.IORuntimeException; import org.koshinuke.util.RandomUtil; import com.google.common.base.Charsets; import com.google.common.base.Function; import com.google.common.io.ByteStreams; import com.google.common.io.Files; import com.google.common.io.NullOutputStream; /** * @author taichi */ public class GitDelegate { final Logger LOG = Logger.getLogger(GitDelegate.class.getName()); final Configuration config; public GitDelegate(Configuration config) { this.config = config; } public List<RepositoryModel> listRepository() { final List<RepositoryModel> repos = new ArrayList<>(); try { Path dir = this.config.getRepositoryRootDir(); try (DirectoryStream<Path> parentStream = java.nio.file.Files .newDirectoryStream(dir)) { for (Path parent : parentStream) { try (DirectoryStream<Path> kidsStream = java.nio.file.Files .newDirectoryStream(parent)) { for (Path maybeRepo : kidsStream) { RepositoryModel repo = this.to(maybeRepo); if (repo != null) { repos.add(repo); } } } } } } catch (IOException e) { this.LOG.log(Level.WARNING, e.getMessage(), e); } return repos; } protected RepositoryModel to(Path maybeRepo) { try { return GitUtil.handleLocal(maybeRepo, new Function<Repository, RepositoryModel>() { @Override public RepositoryModel apply(Repository repo) { return new RepositoryModel(GitDelegate.this.config .getGitHost(), repo); } }); } catch (IORuntimeException e) { this.LOG.log(Level.WARNING, e.getMessage(), e); } return null; } public boolean initRepository(final KoshinukePrincipal p, String name, String readme) { Path repoRoot = this.config.getRepositoryRootDir(); Path path = repoRoot.resolve(name).normalize(); if (path.startsWith(repoRoot) && path.equals(repoRoot) == false) { File newrepo = path.toFile(); if (newrepo.exists() == false) { this.init(p, newrepo, readme); return true; } } return false; } protected void init(final KoshinukePrincipal p, final File newrepo, final String readme) { Git initialized = null; final File working = pickWorkingDir(this.config.getWorkingDir()); try { initialized = Git.init().setBare(true).setDirectory(newrepo).call(); GitUtil.handleClone(newrepo.toURI(), working, new Function<Git, _>() { @Override public _ apply(Git g) { try { // TODO config or input ? File readmeFile = new File(working, "README"); Files.write(readme, readmeFile, Charsets.UTF_8); g.add().addFilepattern(readmeFile.getName()) .call(); PersonIdent commiter = GitDelegate.this.config .getSystemIdent(); PersonIdent author = GitDelegate.this .makeAuthorIdent(p, commiter); // TODO config or input ? g.commit().setMessage("initial commit.") .setCommitter(commiter) .setAuthor(author).call(); g.push().call(); } catch (IOException e) { throw new IORuntimeException(e); } catch (GitAPIException e) { throw new IllegalStateException(e); } return _._; } }); } catch (GitAPIException e) { throw new IllegalStateException(e); } finally { GitUtil.close(initialized); this.delete(working); } } protected void delete(File f) { try { FileUtils.delete(f, FileUtils.RECURSIVE | FileUtils.SKIP_MISSING); } catch (IOException e) { throw new IORuntimeException(e); } } protected static File pickWorkingDir(Path root) { Path working = null; do { working = root.resolve(RandomUtil.nextString()); } while (java.nio.file.Files.exists(working)); return working.toFile(); } public boolean cloneRepository(final KoshinukePrincipal p, String uri, String un, String up) { try { URIish u = new URIish(uri); if (StringUtils.isEmptyOrNull(un) == false && StringUtils.isEmptyOrNull(up) == false) { u = u.setUser(un).setPass(up); } // ローカルディレクトリの読み取りは許可しない。 // TODO support ssh+git if (StringUtils.isEmptyOrNull(u.getHost()) == false) { Path userPath = GitDelegate.this.config.getRepositoryRootDir() .resolve(p.getName()); String humanish = u.getHumanishName(); // TODO 特定のプロジェクトに紐付けるのが正しいのでは無いか? Path newone = userPath.resolve(humanish); File local = java.nio.file.Files.exists(newone) ? pickWorkingDir(userPath) : newone.toFile(); Git g = null; try { g = Git.cloneRepository().setURI(uri).setBare(true) .setDirectory(local).call(); } catch (JGitInternalException e) { this.LOG.log(Level.WARNING, e.getMessage(), e); this.delete(local); throw e; } finally { GitUtil.close(g); } return true; } } catch (Exception e) { this.LOG.log(Level.WARNING, e.getMessage(), e); } return false; } public List<NodeModel> listRepository(String project, String repository, final String rev, final int offset, final int limit) { return this.handleLocal(project, repository, new Function<Repository, List<NodeModel>>() { @Override public List<NodeModel> apply(Repository repo) { return GitDelegate.this.walkRepository(repo, rev, offset, limit); } }); } protected List<NodeModel> walkRepository(final Repository repo, String rev, final int offset, final int limit) { final WalkingContext context = new WalkingContext(rev); final ObjectId oid = this.findRootObject(repo, context); if (oid != null) { return GitUtil.walk(repo, new Function<RevWalk, List<NodeModel>>() { @Override public List<NodeModel> apply(RevWalk walk) { Map<String, NodeModel> map = GitDelegate.this.walkTree( walk, repo, oid, context, offset, limit); if (0 < map.size()) { return GitDelegate.this.walkCommits(walk, repo, oid, context.root, map); } return Collections.emptyList(); } }); } return Collections.emptyList(); } protected ObjectId findRootObject(Repository repo, WalkingContext context) { ObjectId result = null; Ref ref = this.findRef(repo, context); if (ref == null) { String maybeoid = context.rev; int i = maybeoid.indexOf('/'); if (0 < i) { maybeoid = maybeoid.substring(0, i); context.root = maybeoid; context.resource = maybeoid.substring(i + 1); } if (maybeoid.length() == Constants.OBJECT_ID_STRING_LENGTH) { ObjectId oid = ObjectId.fromString(maybeoid); try { ObjectLoader ol = repo.open(oid); switch (ol.getType()) { case Constants.OBJ_COMMIT: case Constants.OBJ_TAG: result = oid; break; default: break; } } catch (IOException e) { // do nothing. this.LOG.log(Level.WARNING, e.getMessage(), e); } } } else { result = ref.getObjectId(); } return result; } protected Ref findRef(Repository repo, WalkingContext context) { Ref r = this.findRef(GitUtil.getBranches(repo), context); if (r == null) { r = this.findRef(repo.getTags(), context); } return r; } protected Ref findRef(Map<String, Ref> refs, WalkingContext context) { String rev = context.rev; for (String s : refs.keySet()) { if (rev.startsWith(s)) { context.root = s; if (s.length() < rev.length()) { context.resource = rev.substring(s.length() + 1); } return refs.get(s); } } return null; } protected Map<String, NodeModel> walkTree(RevWalk walk, Repository repo, ObjectId oid, WalkingContext context, int offset, int limit) { Map<String, NodeModel> result = new HashMap<>(); TreeWalk tw = new TreeWalk(repo); tw.setRecursive(false); String parentPath = context.resource; try { tw.reset(walk.parseTree(oid)); List<WalkingCandidate> candidates = new ArrayList<>(); WalkingCandidate current = null; int depth = 0; do { while (tw.next()) { if (current != null && current.nm != null) { current.nm.addChildren(); } if (result.size() < limit) { WalkingCandidate cand = current; List<String> names = new ArrayList<>(); String name = tw.getNameString(); names.add(name); while (cand != null) { names.add(cand.name); cand = cand.parent; } Collections.reverse(names); String path = this.toPath(names); NodeModel nm = this.makeModel(walk, path, name, tw.getObjectId(depth)); boolean startsWith = parentPath.isEmpty() || parentPath.length() < path.length() && path.startsWith(parentPath); if ((offset < 1 || 0 < offset--) && startsWith) { result.put(path, nm); } if (tw.isSubtree() && (startsWith || parentPath.startsWith(path))) { candidates.add(new WalkingCandidate(tw .getObjectId(depth), tw.getNameString(), current, nm)); } } } if (0 < candidates.size()) { WalkingCandidate c = candidates.remove(0); tw.addTree(c.oid); depth++; current = c; } else { current = null; } } while (current != null); } catch (IOException e) { throw new IORuntimeException(e); } finally { tw.release(); } return result; } protected String toPath(List<String> path) { StringBuilder stb = new StringBuilder(); for (Iterator<String> i = path.iterator(); i.hasNext();) { String s = i.next(); stb.append(s); if (i.hasNext()) { stb.append('/'); } } return stb.toString(); } protected NodeModel makeModel(RevWalk walk, String path, String name, ObjectId oid) { try { NodeModel nm = new NodeModel(path, name); nm.setObject(walk.parseAny(oid)); return nm; } catch (IOException e) { throw new IORuntimeException(e); } } // TODO リポジトリのログが増えるとコストが高くなりがちなので、遡るコミット数にしきい値を設け、 // それで解決出来ないものは、再リクエストによって遅延ロードする仕組み。 protected List<NodeModel> walkCommits(RevWalk walk, Repository repo, ObjectId objectId, String root, Map<String, NodeModel> map) { List<NodeModel> result = new ArrayList<>(map.size()); try { RevCommit commit = walk.parseCommit(objectId); walk.reset(); walk.markStart(commit); List<PathFilter> filters = new ArrayList<>(); for (Iterator<String> i = map.keySet().iterator(); i.hasNext();) { String path = i.next(); NodeModel nm = map.get(path); if (nm.getObject().getType() == Constants.OBJ_BLOB) { filters.add(PathFilter.create(path)); } else { i.remove(); this.addResult(result, nm, root); } } TreeFilter tf = PathFilterGroup.create(filters); walk.setTreeFilter(tf); DiffFormatter diffFmt = this.makeDiffFormatter(repo, tf); outer: for (RevCommit rc : walk) { final RevTree now = rc.getTree(); if (0 < rc.getParentCount()) { final RevTree pre = rc.getParent(0).getTree(); for (DiffEntry de : diffFmt.scan(now, pre)) { NodeModel nm = this.remove(map, de.getNewPath(), de.getOldPath()); this.setLastCommit(result, rc, nm, root); if (map.isEmpty()) { break outer; } } } else { TreeWalk tw = new TreeWalk(repo); tw.reset(rc.getTree()); tw.setRecursive(true); try { while (tw.next()) { String path = tw.getPathString(); NodeModel nm = map.remove(path); this.setLastCommit(result, rc, nm, root); if (map.isEmpty()) { break outer; } } } finally { tw.release(); } } } return result; } catch (IOException e) { throw new IORuntimeException(e); } } protected DiffFormatter makeDiffFormatter(Repository repo, TreeFilter tf) { // see. org.eclipse.jgit.diff.DiffConfig DiffFormatter diffFmt = new DiffFormatter(new NullOutputStream()); diffFmt.setRepository(repo); diffFmt.setPathFilter(tf); return diffFmt; } protected NodeModel remove(Map<String, NodeModel> diffCand, String l, String r) { NodeModel nm = diffCand.remove(l); if (nm == null) { nm = diffCand.remove(r); } return nm; } protected void setLastCommit(List<NodeModel> result, RevCommit rc, NodeModel nm, String root) { if (nm != null) { nm.setLastCommit(rc); this.addResult(result, nm, root); } } protected void addResult(List<NodeModel> result, NodeModel nm, String root) { nm.setPath(root + "/" + nm.getPath()); result.add(nm); } public BlobModel getBlob(String project, String repository, final String rev) { return this.handleLocal(project, repository, new Function<Repository, BlobModel>() { @Override public BlobModel apply(Repository repo) { return GitDelegate.this.findBlob(repo, rev); } }); } protected BlobModel findBlob(final Repository repo, String rev) { final WalkingContext context = new WalkingContext(rev); final ObjectId oid = this.findRootObject(repo, context); if (oid != null) { return GitUtil.walk(repo, new Function<RevWalk, BlobModel>() { @Override public BlobModel apply(RevWalk walk) { BlobModel bm = GitDelegate.this.findBlob(walk, repo, oid, context); GitDelegate.this.walkCommits(walk, repo, oid, context, bm); return bm; } }); } return null; } protected BlobModel findBlob(RevWalk walk, Repository repo, ObjectId oid, WalkingContext context) { try { final BlobModel bm = new BlobModel(); this.setContent(repo, walk.parseTree(oid), context.resource, new BlobHolder() { @Override public void setId(ObjectId oid) { bm.setObjectId(oid); } @Override public void setContent(String content) { bm.setContent(content); } }); return bm; } catch (IOException e) { throw new IORuntimeException(e); } } protected void walkCommits(RevWalk walk, Repository repo, ObjectId oid, WalkingContext context, BlobModel bm) { try { RevCommit commit = walk.parseCommit(oid); walk.reset(); walk.markStart(commit); TreeFilter tf = PathFilter.create(context.resource); walk.setTreeFilter(tf); DiffFormatter diffFmt = this.makeDiffFormatter(repo, tf); outer: for (RevCommit rc : walk) { final RevTree now = rc.getTree(); if (0 < rc.getParentCount()) { final RevTree pre = rc.getParent(0).getTree(); for (DiffEntry de : diffFmt.scan(now, pre)) { if (context.resource.equals(de.getNewPath()) || context.resource.equals(de.getOldPath())) { bm.setLastCommit(rc); break outer; } } } else { TreeWalk tw = new TreeWalk(repo); tw.reset(rc.getTree()); tw.setRecursive(true); try { while (tw.next()) { if (context.resource.equals(tw.getPathString())) { bm.setLastCommit(rc); break outer; } } } finally { tw.release(); } } } } catch (IOException e) { throw new IORuntimeException(e); } } public BlobModel modifyBlob(final KoshinukePrincipal p, String project, String repository, final String rev, final BlobModel input) { return this.handleLocal(project, repository, new Function<Repository, BlobModel>() { @Override public BlobModel apply(Repository repo) { return GitDelegate.this.modifyBlob(p, repo, rev, input); } }); } protected BlobModel modifyBlob(final KoshinukePrincipal p, final Repository repo, final String rev, final BlobModel input) { final WalkingContext context = new WalkingContext(rev); final ObjectId oid = this.findRootObject(repo, context); if (oid != null) { return GitUtil.walk(repo, new Function<RevWalk, BlobModel>() { @Override public BlobModel apply(RevWalk walk) { return GitDelegate.this.modifyBlob(p, walk, repo, oid, context, input); } }); } return null; } protected BlobModel modifyBlob(KoshinukePrincipal p, RevWalk walk, Repository repo, ObjectId oid, WalkingContext context, BlobModel input) { TreeWalk tw = new TreeWalk(repo); tw.setRecursive(true); try { RevObject ro = walk.parseAny(oid); if (ro.getType() == Constants.OBJ_COMMIT) { tw.reset(walk.parseTree(oid)); tw.setFilter(PathFilter.create(context.resource)); if (tw.next()) { ObjectId o = tw.getObjectId(0); if (o.equals(input.getObjectId())) { RevCommit commit = walk.parseCommit(oid); return this.modifyResource(p, repo, commit, context, input); } } } } catch (IOException e) { throw new IORuntimeException(e); } finally { tw.release(); } return null; } protected BlobModel modifyResource(final KoshinukePrincipal p, Repository repo, final RevCommit commit, final WalkingContext context, final BlobModel input) { final File working = pickWorkingDir(this.config.getWorkingDir()); try { return GitUtil.handleClone(repo.getDirectory().toURI(), working, new Function<Git, BlobModel>() { @Override public BlobModel apply(Git g) { try { boolean create = g.getRepository().getRef( Constants.R_HEADS + context.root) == null; g.checkout().setCreateBranch(create) .setStartPoint(commit) .setName(context.root).call(); File file = new File(working, context.resource); Files.write(input.getContent(), file, Charsets.UTF_8); g.add().addFilepattern(context.resource).call(); PersonIdent commiter = GitDelegate.this.config .getSystemIdent(); PersonIdent author = GitDelegate.this .makeAuthorIdent(p, commiter); RevCommit commit = g.commit() .setMessage(input.getMessage()) .setCommitter(commiter) .setAuthor(author).call(); g.push().call(); input.setLastCommit(commit); return input; } catch (GitAPIException e) { throw new IllegalStateException(e); } catch (IOException e) { throw new IORuntimeException(e); } } }); } finally { this.delete(working); } } protected PersonIdent makeAuthorIdent(final KoshinukePrincipal p, PersonIdent commiter) { PersonIdent author = new PersonIdent(p.getName(), p.getMail(), commiter.getWhen(), commiter.getTimeZone()); return author; } public List<BranchHistoryModel> getHistories(String project, String repository) { return this.handleLocal(project, repository, new Function<Repository, List<BranchHistoryModel>>() { @Override public List<BranchHistoryModel> apply(Repository repo) { return GitDelegate.this.findBranchHistories(repo); } }); } protected List<BranchHistoryModel> findBranchHistories(final Repository repo) { List<BranchHistoryModel> result = new ArrayList<>(); Map<String, Ref> branches = GitUtil.getBranches(repo); for (String s : branches.keySet()) { final BranchHistoryModel model = new BranchHistoryModel(s); final Ref ref = branches.get(s); List<long[]> activities = GitUtil.walk(repo, new Function<RevWalk, List<long[]>>() { @Override public List<long[]> apply(RevWalk walk) { RevCommit commit = GitDelegate.this.setLastCommit( walk, ref, model); return GitDelegate.this.parseActivities(walk, repo, commit); } }); model.setActivities(activities); result.add(model); } return result; } protected RevCommit setLastCommit(RevWalk walk, Ref ref, BranchHistoryModel model) { try { ObjectId oid = ref.getObjectId(); RevCommit commit = walk.parseCommit(oid); model.setLastCommit(commit); return commit; } catch (IOException e) { throw new IORuntimeException(e); } } protected List<long[]> parseActivities(RevWalk walk, Repository repo, RevCommit begin) { List<long[]> result = new ArrayList<>(30); try { walk.reset(); walk.markStart(begin); Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); class TimeContext { long time; int count; } List<TimeContext> list = new ArrayList<>(); for (int i = 0; i < 30; i++) { TimeContext tc = new TimeContext(); tc.time = calendar.getTimeInMillis(); list.add(tc); calendar.add(Calendar.DATE, -1); } for (RevCommit cmt : walk) { PersonIdent author = cmt.getAuthorIdent(); long time = author.getWhen().getTime(); for (TimeContext tc : list) { if (tc.time < time) { tc.count++; break; } } } Collections.reverse(list); for (TimeContext tc : list) { long[] ary = { tc.time / 1000L, tc.count }; result.add(ary); } return result; } catch (IOException e) { throw new IORuntimeException(e); } } protected <T> T handleLocal(String project, String repository, Function<Repository, T> handler) { return GitUtil.handleLocal(this.config.getRepositoryRootDir(), project, repository, handler); } public List<CommitModel> getCommits(String project, String repository, final String rev, final String offset, final int limit) { return this.handleLocal(project, repository, new Function<Repository, List<CommitModel>>() { @Override public List<CommitModel> apply(Repository repo) { return GitDelegate.this.parseCommits(repo, rev, offset, limit); } }); } protected List<CommitModel> parseCommits(final Repository repo, String rev, final String offset, final int limit) { final WalkingContext context = new WalkingContext(rev); final ObjectId oid = this.findRootObject(repo, context); if (oid != null) { return GitUtil.walk(repo, new Function<RevWalk, List<CommitModel>>() { @Override public List<CommitModel> apply(RevWalk walk) { return GitDelegate.this.parseCommits(walk, repo, oid, context, offset, limit); } }); } return Collections.emptyList(); } protected List<CommitModel> parseCommits(RevWalk walk, Repository repo, ObjectId oid, WalkingContext context, String offset, int limit) { try { List<CommitModel> result = new ArrayList<>(); RevCommit begin = null; if (StringUtils.isEmptyOrNull(offset) == false && offset.length() == Constants.OBJECT_ID_STRING_LENGTH) { ObjectId offsetId = ObjectId.fromString(offset); RevCommit rc = walk.parseCommit(offsetId); if (0 < rc.getParentCount()) { begin = rc.getParent(0); } } if (begin == null) { begin = walk.parseCommit(oid); } walk.reset(); walk.markStart(begin); if (StringUtils.isEmptyOrNull(context.resource) == false) { walk.setTreeFilter(AndTreeFilter.create( PathFilter.create(context.resource), TreeFilter.ANY_DIFF)); } for (RevCommit rc : walk) { if (limit-- < 1) { break; } result.add(new CommitModel(rc)); } return result; } catch (IOException e) { throw new IORuntimeException(e); } } public DiffModel getDiff(String project, String repository, final String commitid) { return this.handleLocal(project, repository, new Function<Repository, DiffModel>() { @Override public DiffModel apply(final Repository repo) { return GitUtil.walk(repo, new Function<RevWalk, DiffModel>() { @Override public DiffModel apply(RevWalk walk) { return GitDelegate.this.getDiff(walk, repo, commitid); } }); } }); } protected DiffModel getDiff(RevWalk walk, Repository repo, String commitid) { try { ObjectId oid = repo.resolve(commitid); RevCommit current = walk.parseCommit(oid); DiffModel result = new DiffModel(current); result.setLastCommit(current); if (0 < current.getParentCount()) { ByteArrayOutputStream out = new ByteArrayOutputStream(1024); DiffFormatter fmt = new SimpleDiffFormatter(out); fmt.setRepository(repo); // TODO whitespaceの扱いを設定できる様にする? fmt.setDiffComparator(RawTextComparator.WS_IGNORE_CHANGE); fmt.setDetectRenames(true); // TODO git note を、どう扱うか考える。 List<DiffEntryModel> list = new ArrayList<>(); RevTree oldTree = walk.parseCommit(current.getParent(0)) .getTree(); RevTree newTree = current.getTree(); for (DiffEntry de : fmt.scan(oldTree, newTree)) { DiffEntryModel dm = new DiffEntryModel(de); fmt.format(de); fmt.flush(); dm.setPatch(out.toString(Charsets.UTF_8.name())); out.reset(); this.setContent(repo, oldTree, newTree, de, dm); list.add(dm); } result.setDiff(list); } return result; } catch (IOException e) { throw new IORuntimeException(e); } } protected void setContent(Repository repo, RevTree oldTree, RevTree newTree, DiffEntry de, final DiffEntryModel dm) throws IOException { class H implements BlobHolder { @Override public void setId(ObjectId oid) { } @Override public void setContent(String content) { } } this.setContent(repo, oldTree, de.getOldPath(), new H() { @Override public void setContent(String content) { dm.setOldContent(content); } }); this.setContent(repo, newTree, de.getNewPath(), new H() { @Override public void setContent(String content) { dm.setNewContent(content); } }); } interface BlobHolder { void setId(ObjectId oid); void setContent(String content); } protected void setContent(Repository repo, RevTree tree, String path, BlobHolder holder) throws IOException { TreeWalk tw = new TreeWalk(repo); try { tw.setRecursive(true); tw.reset(tree); tw.setFilter(PathFilter.create(path)); if (tw.next()) { ObjectId o = tw.getObjectId(0); ObjectLoader ol = tw.getObjectReader().open(o, Constants.OBJ_BLOB); try (ObjectStream in = ol.openStream()) { // TODO コンテンツがデカ過ぎる場合、メモリに全部展開してしまうのは良くないので、 // クライアント側に対してリソースが大きすぎる事を通知した上で、再リクエストしてもらう仕組みを作りこむ。 byte[] bytes = ByteStreams.toByteArray(in); StringBuilder stb = this.toDataScheme(path); if (stb == null) { // TODO from config? // see. com.ibm.icu.text.CharsetDetector // UTF-8以外のモノが混ざる様ならコンテンツの文字エンコーディングをここでUTF-8に変換する必要がある。 holder.setContent(new String(bytes, Charsets.UTF_8)); } else { stb.append(Base64.encodeBytes(bytes)); holder.setContent(stb.toString()); } holder.setId(o); } } } finally { tw.release(); } } static final Pattern isImages = Pattern.compile( "\\.(jpe?g|gif|png|ico|bmp)$", Pattern.CASE_INSENSITIVE); // see. http://tools.ietf.org/html/rfc2397 protected StringBuilder toDataScheme(String path) { Matcher m = isImages.matcher(path); if (m.find()) { StringBuilder stb = new StringBuilder(); stb.append("data:image/"); stb.append(m.group(1)); stb.append(";base64,"); return stb; } return null; } public List<BlameModel> getBlame(String project, String repository, final String rev) { return this.handleLocal(project, repository, new Function<Repository, List<BlameModel>>() { @Override public List<BlameModel> apply(Repository repo) { return GitDelegate.this.parseBlame(repo, rev); } }); } protected List<BlameModel> parseBlame(Repository repo, String rev) { final WalkingContext context = new WalkingContext(rev); final ObjectId oid = this.findRootObject(repo, context); if (oid != null) { try { Git g = new Git(repo); BlameResult br = g.blame().setFilePath(context.resource) .setStartCommit(oid).setFollowFileRenames(true).call(); RawText rt = br.getResultContents(); List<BlameModel> list = new ArrayList<>(); for (int i = 0, l = rt.size(); i < l; i++) { RevCommit rc = br.getSourceCommit(i); BlameModel bm = new BlameModel(rc); bm.setContent(rt.getString(i)); list.add(bm); } return list; } catch (GitAPIException e) { throw new IllegalStateException(e); } } return null; } }