/***************************************************************************** This file is part of Git-Starteam. Git-Starteam is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Git-Starteam is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Git-Starteam. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package org.sync.githelper; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import org.ossnoize.git.fastimport.CatBlob; import org.ossnoize.git.fastimport.Commit; import org.ossnoize.git.fastimport.DataRef; import org.ossnoize.git.fastimport.Feature; import org.ossnoize.git.fastimport.FileDelete; import org.ossnoize.git.fastimport.FileOperation; import org.ossnoize.git.fastimport.Sha1Ref; import org.ossnoize.git.fastimport.enumeration.FeatureType; import org.sync.ErrorEater; import org.sync.Log; import org.sync.RepositoryHelper; import org.sync.util.FileUtility; import org.sync.util.LogEntry; import org.sync.util.SmallRef; import org.sync.util.StarteamFileInfo; import org.sync.util.enumeration.FileStatusStyle; public class GitHelper extends RepositoryHelper { private final static String STARTEAMFILEINFODIR = "starteam"; private final static String STARTEAMFILEINFO = "StarteamFileInfo.gz"; private String gitExecutable; private Process gitFastImport; private Thread gitFastImportOutputEater; private Thread gitFastImportErrorEater; private GitFastImportOutputReader gitResponse; private int debugFileCounter = 0; private Map<String, Map<String, DataRef>> trackedFiles; private boolean isBare; public GitHelper(String preferedPath, boolean createRepo, String workingDir) throws Exception { if (!findExecutable(preferedPath)) { throw new Exception("Git executable not found."); } if(workingDir == null) { workingDir = System.getProperty("user.dir"); } setWorkingDirectory(workingDir, createRepo); trackedFiles = Collections.synchronizedMap(new HashMap<String, Map<String, DataRef>>()); loadFileInformation(); } private boolean findExecutable(String preferedPath) { String os = System.getProperty("os.name"); if(null != preferedPath) { String fileExtension = ""; if(os.contains("indow")) { fileExtension = ".exe"; } File gitExec = new File(preferedPath + File.separator + "git" + fileExtension); if(gitExec.exists() && gitExec.canExecute()) { try { gitExecutable = gitExec.getCanonicalPath(); } catch (IOException e) { e.printStackTrace(); } } } else { if(os.contains("indow")) { File gitExec = new File("C:" + File.separator + "Program Files" + File.separator + "Git" + File.separator + "bin" + File.separator + "git.exe"); if(gitExec.exists() && gitExec.canExecute()) { try { gitExecutable = gitExec.getCanonicalPath(); } catch (IOException e) { e.printStackTrace(); } } else { gitExec = new File("C:" + File.separator + "Program Files (x86)" + File.separator + "Git" + File.separator + "bin" + File.separator + "git.exe"); if(gitExec.exists() && gitExec.canExecute()) { try { gitExecutable = gitExec.getCanonicalPath(); } catch (IOException e) { e.printStackTrace(); } } } } else { gitExecutable = "git"; } } return (null != gitExecutable); } private boolean repositoryExists(boolean create) { if (isValidGitRepository()) { return true; } else if (create) { ProcessBuilder process = new ProcessBuilder(); process.command(gitExecutable, "init", "--bare"); process.directory(new File(repositoryDir)); try { Process init = process.start(); Thread errorEater = new Thread(new ErrorEater(init.getErrorStream(), "init")); Thread stdOutEater = new Thread(new ErrorEater(init.getInputStream(), "init", true)); init.waitFor(); errorEater.join(); stdOutEater.join(); } catch (Exception e) { e.printStackTrace(); return false; } return true; } return false; } private void grabTrackedFiles(String head) { ProcessBuilder process = new ProcessBuilder(); process.command(gitExecutable, "ls-tree", "--full-tree" ,"-r", head); process.directory(new File(repositoryDir)); try { Process lsFiles = process.start(); Thread gitQueryWorker = new Thread(new GitLsFilesReader(lsFiles.getInputStream(), head)); Thread gitErrorStreamEater = new Thread(new ErrorEater(lsFiles.getErrorStream(), "ls-tree")); gitQueryWorker.start(); gitErrorStreamEater.start(); int returnCode = lsFiles.waitFor(); gitQueryWorker.join(); gitErrorStreamEater.join(); if(0 != returnCode) { trackedFiles.put(head, new HashMap<String, DataRef>()); } } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } private boolean isValidGitRepository() { ProcessBuilder process = new ProcessBuilder(); process.command(gitExecutable, "branch"); process.directory(new File(repositoryDir)); try { Process status = process.start(); Thread statusOut = new Thread(new ErrorEater(status.getInputStream(), true)); Thread statusErr = new Thread(new ErrorEater(status.getErrorStream(), "branch")); statusOut.start(); statusErr.start(); int result = status.waitFor(); statusOut.join(); statusErr.join(); return (result == 0); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return false; } @Override public boolean isBareRepository() { File repo = new File(repositoryDir); if(repo.isDirectory()) { for(File f : repo.listFiles()) { // first possible case, it is a normal repository. if(f.getName().equalsIgnoreCase(".git") && f.isDirectory()) { File config = new File(f.getAbsolutePath() + File.separator + "config"); if(config.exists()) { return checkIsBare(config); } } if(f.getName().equalsIgnoreCase("config") && f.isFile()) { return checkIsBare(f); } } } return false; } private boolean checkIsBare(File config) { FileReader freader = null; BufferedReader buffer = null; try { freader = new FileReader(config); buffer = new BufferedReader(freader); String line; while(null != (line = buffer.readLine())) { int equalsPosition = line.indexOf('='); if(equalsPosition >= 0) { String var = line.substring(0, equalsPosition).trim(); String value = line.substring(equalsPosition+1).trim(); if(var.equalsIgnoreCase("bare")) { return value.equalsIgnoreCase("true"); } } } } catch (IOException e) { e.printStackTrace(); } finally { FileUtility.close(buffer, freader); } return false; } @Override public void dispose() { super.dispose(); try { if(null != gitFastImport) { int endCode = gitFastImport.waitFor(); if(endCode != 0) { Log.log("Git fast-import has finished anormally with code:" + endCode); } gitFastImportOutputEater.join(); gitFastImportErrorEater.join(); gitFastImport = null; } } catch (InterruptedException e) { e.printStackTrace(); } } @Override public boolean isSpecialFile(String filename) { File testFile = new File(filename); if(testFile.getName().equalsIgnoreCase(".gitignore")) { return true; } else if (testFile.getName().equalsIgnoreCase(".gitattributes")) { return true; } return false; } @Override public void writeCommit(Commit commit) throws IOException { super.writeCommit(commit); String headName = commit.getReference(); for(FileOperation ops : commit.getFileOperation()) { if(null != ops.getMark()) { if(!trackedFiles.containsKey(headName)) { trackedFiles.put(headName, new HashMap<String, DataRef>()); } trackedFiles.get(headName).put(ops.getPath(), ops.getMark()); } else if(ops instanceof FileDelete) { // This shouldn't be null, but sometimes is. I think the // file janitor is getting invoked, too. The problems may // have the same cause. Map<String, DataRef> headTracked = trackedFiles.get(headName); if (headTracked != null) { headTracked.remove(ops.getPath()); } } } } @Override public int gc() { int ret = Integer.MIN_VALUE; ProcessBuilder process = new ProcessBuilder(); process.command(gitExecutable, "gc"); process.directory(new File(repositoryDir)); try { Process gitGc = process.start(); Thread gitErrorStreamEater = new Thread(new ErrorEater(gitGc.getErrorStream(), "gc")); Thread gitQueryWorker = new Thread(new ErrorEater(gitGc.getInputStream(), "gc")); gitErrorStreamEater.start(); gitQueryWorker.start(); ret = gitGc.waitFor(); gitErrorStreamEater.join(); gitQueryWorker.join(); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return ret; } @Override protected OutputStream getFastImportStream() { if(null == fastExportOverrideToFile) { if(null == gitFastImport) { ProcessBuilder process = new ProcessBuilder(); process.command(gitExecutable, "fast-import", "--done"); process.directory(new File(repositoryDir)); try { gitFastImport = process.start(); gitResponse = new GitFastImportOutputReader(gitFastImport.getInputStream()); gitFastImportOutputEater = new Thread(gitResponse); gitFastImportErrorEater = new Thread(new ErrorEater(gitFastImport.getErrorStream(), "fast-import")); gitFastImportOutputEater.start(); gitFastImportErrorEater.start(); OutputStream out = gitFastImport.getOutputStream(); // Validate Feature needed; Feature feature = new Feature(FeatureType.DateFormat, "raw"); feature.writeTo(out); Feature catBlb = new Feature(FeatureType.CatBlob); catBlb.writeTo(out); } catch (IOException e) { e.printStackTrace(); return null; } } return gitFastImport.getOutputStream(); } else { try { return new FileOutputStream(fastExportOverrideToFile.getPath() + "." + (debugFileCounter++)); } catch (FileNotFoundException e) { e.printStackTrace(); } return null; } } @Override public boolean isFastImportRunning() { if(null == fastExportOverrideToFile && null != gitFastImport ) { try { gitFastImport.exitValue(); } catch (IllegalThreadStateException e) { return true; } } return false; } @Override public Set<String> getListOfTrackedFile(String head) { if(!trackedFiles.containsKey(head)) { grabTrackedFiles(head); } if(!trackedFiles.containsKey(head)) { return null; } return trackedFiles.get(head).keySet(); } @Override public Date getLastCommitOfBranch(String branchName) { SmallRef to = new SmallRef(branchName); List<LogEntry> commits = getCommitLog(to.back(1), to); if (commits.size() <= 0) { commits = getCommitLog(to); } return commits.get(0).getTimeOfCommit(); } @Override public List<LogEntry> getCommitLog(SmallRef from, SmallRef to) { ProcessBuilder process = new ProcessBuilder(); String refs; if(null == from) { refs = to.getRef(); } else { refs = from.getRef() + ".." + to.getRef(); } process.command(gitExecutable, "log", "--date=iso8601", "--find-renames=75", "--full-index", "--find-copies=75", "--raw", refs); process.directory(new File(repositoryDir)); try { Process log = process.start(); GitLogReader logReader = new GitLogReader(log.getInputStream()); Thread logReaderThread = new Thread(logReader); Thread errorEater = new Thread(new ErrorEater(log.getErrorStream(), "log:=" + to.getRef())); logReaderThread.start(); errorEater.start(); log.waitFor(); logReaderThread.join(); errorEater.join(); return logReader.getEntries(); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return null; } @Override public List<LogEntry> getCommitLog(SmallRef to) { return getCommitLog(null, to); } private File buildStarteamInfoDir() { File objDir = new File(repositoryDir + (isBare?"":File.separator + ".git") + File.separator + STARTEAMFILEINFODIR); if(!objDir.exists()) { objDir.mkdir(); } File objFile = new File(objDir.getAbsolutePath() + File.separator + STARTEAMFILEINFO); return objFile; } @Override @SuppressWarnings("unchecked") protected boolean loadFileInformation() { FileInputStream fin = null; ObjectInputStream objin = null; File objFile = buildStarteamInfoDir(); if(!objFile.exists()) return false; try { fin = new FileInputStream(objFile); objin = new ObjectInputStream(fin); Object tempObject = objin.readObject(); if(tempObject instanceof Map) { fileInformation = (Map<String, Map<String, StarteamFileInfo>>) tempObject; } } catch (IOException e) { e.printStackTrace(); return false; } catch (ClassNotFoundException e) { e.printStackTrace(); return false; } finally { FileUtility.close(objin, fin); } return true; } @Override protected void saveFileInformation() { if(null != fileInformation) { FileOutputStream fout = null; ObjectOutputStream objout = null; File objFile = buildStarteamInfoDir(); try { fout = new FileOutputStream(objFile); objout = new ObjectOutputStream(fout); objout.writeObject(fileInformation); } catch (IOException e) { e.printStackTrace(); } finally { FileUtility.close(objout, fout); } } } @Override public void setWorkingDirectory(String dir, boolean create) { super.setWorkingDirectory(dir, create); if (!repositoryExists(create)) { Log.log("Destination repository not found in '" + repositoryDir + "'"); } isBare = isBareRepository(); } @Override public String getWorkingDirectory() { if(isBare) { return repositoryDir; } return repositoryDir + java.io.File.separator + ".git"; } @Override public void getFileContent(String head, String path, OutputStream whereToStore) { if(!trackedFiles.containsKey(head)) { grabTrackedFiles(head); } if(trackedFiles.containsKey(head)) { if(trackedFiles.get(head).containsKey(path)) { OutputStream gitFastImportStream = getFastImportStream(); // Make sure fast import process is started DataRef fileRef = trackedFiles.get(head).get(path); CatBlob request = new CatBlob(fileRef); try { gitResponse.setCatBlobStream(whereToStore); request.writeTo(gitFastImportStream); gitResponse.waitForCatBlob(); } catch (IOException ex) { Log.logf("Failed to cat-blob the path <%s> on head <%s>:%s", path, head, ex); } gitResponse.setCatBlobStream(null); } } } private class GitLsFilesReader implements Runnable { private InputStream input; private String head; private GitLsFilesReader(InputStream in, String head) { this.input = in; this.head = head; } @Override public void run() { synchronized (trackedFiles) { InputStreamReader reader = null; BufferedReader buffer = null; try { reader = new InputStreamReader(input); buffer = new BufferedReader(reader); String file = null; while(null != (file = buffer.readLine())) { String path = file.substring(53).trim(); String sha1 = file.substring(12, 52).trim(); String type = file.substring(6,12).trim(); if(type.equalsIgnoreCase("blob")) { if(!trackedFiles.containsKey(head)) { trackedFiles.put(head, new HashMap<String, DataRef>()); } trackedFiles.get(head).put(path, new Sha1Ref(sha1)); } } } catch (IOException e) { e.printStackTrace(); } finally { FileUtility.close(buffer, reader); } } } } private class GitLogReader implements Runnable { private static final String dateKey = "Date:"; private static final String shaKey = "commit"; private static final String authorKey = "Author:"; private List<LogEntry> entries; private DataRef commitSHA; private InputStream logStream; public GitLogReader(InputStream stream) { logStream = stream; entries = Collections.synchronizedList(new ArrayList<LogEntry>()); } public List<LogEntry> getEntries() { return entries; } @Override public void run() { SimpleDateFormat dateFormatIso = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); InputStreamReader reader = null; BufferedReader buffer = null; try { reader = new InputStreamReader(logStream); buffer = new BufferedReader(reader); LogEntry entry = null; String line; while((line = buffer.readLine()) != null) { if(line.startsWith(dateKey)) { String isoDate = line.substring(dateKey.length()).trim(); try { entry.setTimeOfCommit(dateFormatIso.parse(isoDate)); } catch (ParseException e) { throw new Error("The date " + isoDate + " is not a valid (git wise) iso 8601 date"); } } else if (line.startsWith(shaKey)) { // Git log entry always start with the commit SHA commitSHA = new Sha1Ref(line.substring(shaKey.length()).trim()); entry = new LogEntry(commitSHA); synchronized (entries) { entries.notify(); entries.add(entry); } } else if (line.startsWith(authorKey)) { entry.setAuthor(line.substring(authorKey.length()).trim()); } else if (line.startsWith(":")) { entry.parseStatusLine(FileStatusStyle.GitRaw, line); } else { entry.appendComment(line.trim() + "\n"); } } } catch (IOException e) { e.printStackTrace(); } finally { FileUtility.close(buffer, reader); } } } private class GitFastImportOutputReader implements Runnable { private final Lock outputReaderLock = new ReentrantLock(); private final Condition outputCatBlobCondition = outputReaderLock.newCondition(); private InputStream stream; private String currentHead; private Pattern ls = Pattern.compile("^[0-9]{6} [a-z]+ [0-9a-fA-F]{40}\t.+$"); private Pattern catblob = Pattern.compile("^[0-9a-fA-F]{40} blob [0-9]+$"); private OutputStream catBlobStream; private boolean catBlobInProgress; public GitFastImportOutputReader(InputStream stream) { this.stream = stream; } public void setCurrentHead(String currentHead) { this.currentHead = currentHead; } public void setCatBlobStream(OutputStream stream) { catBlobInProgress = true; catBlobStream = stream; } public void waitForCatBlob() { outputReaderLock.lock(); try { while (catBlobInProgress) { try { outputCatBlobCondition.await(); } catch (InterruptedException ex) { Logger.getLogger(GitHelper.class.getName()).log(Level.SEVERE, null, ex); } } } finally { outputReaderLock.unlock(); } } public void run() { StringBuilder firstResponse = new StringBuilder(50); try { int character; do { character = stream.read(); while(character >= 0) { if(character == '\n') { break; } firstResponse.append((char)character); character = stream.read(); } if (ls.matcher(firstResponse).matches()) { outputReaderLock.lock(); try { String type = firstResponse.substring(6, 10); if(type.equalsIgnoreCase("blob")) { String sha1 = firstResponse.substring(13,53); String filename = firstResponse.substring(54).trim(); trackedFiles.get(currentHead).put(filename, new Sha1Ref(sha1)); } } finally { outputReaderLock.unlock(); } } else if(catblob.matcher(firstResponse).matches()) { outputReaderLock.lock(); try { // In case we have a cat-blob response String strSize = firstResponse.substring(45).trim(); long size; try { size = Long.parseLong(strSize); } catch(NumberFormatException ex) { continue; // Invalid number go to the next iteration } // we need to read the full object regardless of it's size byte[] buffer = new byte[Math.min((int)size, 4096)]; int readSize = stream.read(buffer); while(readSize >= 0) { catBlobStream.write(buffer, 0, readSize); size -= readSize; if(size <= 0) { break; } readSize = stream.read(buffer); } // Notify we are finished catBlobInProgress = false; outputCatBlobCondition.signalAll(); } finally { outputReaderLock.unlock(); } } else if (firstResponse.toString().trim().length() > 0) { System.err.println("Unknown response <" + firstResponse + ">"); } firstResponse.setLength(0); } while(character >= 0); } catch (IOException e) { e.printStackTrace(); } } } }