/************************************************************************** OmegaT - Computer Assisted Translation (CAT) tool with fuzzy matching, translation memory, keyword search, glossaries, and translation leveraging into updated projects. Copyright (C) 2012 Alex Buloichik 2014 Alex Buloichik, Aaron Madlon-Kay Home page: http://www.omegat.org/ Support center: http://groups.yahoo.com/group/OmegaT/ This file is part of OmegaT. OmegaT 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. OmegaT 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 this program. If not, see <http://www.gnu.org/licenses/>. **************************************************************************/ package org.omegat.core.team2.impl; import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import javax.xml.namespace.QName; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.LsRemoteCommand; import org.eclipse.jgit.api.ResetCommand.ResetType; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.InvalidRemoteException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.TransportException; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheIterator; 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.lib.StoredConfig; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.PushResult; import org.eclipse.jgit.transport.RemoteRefUpdate; import org.eclipse.jgit.transport.RemoteRefUpdate.Status; import org.eclipse.jgit.treewalk.AbstractTreeIterator; import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.omegat.core.team2.IRemoteRepository2; import org.omegat.core.team2.ProjectTeamSettings; import org.omegat.util.Log; import gen.core.project.RepositoryDefinition; /** * GIT repository connection implementation. * * @author Alex Buloichik (alex73mail@gmail.com) * @author Aaron Madlon-Kay */ public class GITRemoteRepository2 implements IRemoteRepository2 { private static final Logger LOGGER = Logger.getLogger(GITRemoteRepository2.class.getName()); protected static final String LOCAL_BRANCH = "master"; protected static final String REMOTE_BRANCH = "origin/master"; protected static final String REMOTE = "origin"; String repositoryURL; File localDirectory; protected Repository repository; static { CredentialsProvider.setDefault(new GITCredentialsProvider()); } @Override public void init(RepositoryDefinition repo, File dir, ProjectTeamSettings teamSettings) throws Exception { repositoryURL = repo.getUrl(); localDirectory = dir; String predefinedUser = repo.getOtherAttributes().get(new QName("gitUsername")); String predefinedPass = repo.getOtherAttributes().get(new QName("gitPassword")); String predefinedFingerprint = repo.getOtherAttributes().get(new QName("gitFingerprint")); ((GITCredentialsProvider) CredentialsProvider.getDefault()).setPredefinedCredentials(repositoryURL, predefinedUser, predefinedPass, predefinedFingerprint); ((GITCredentialsProvider) CredentialsProvider.getDefault()).setTeamSettings(teamSettings); File gitDir = new File(localDirectory, ".git"); if (gitDir.exists() && gitDir.isDirectory()) { // already cloned repository = Git.open(localDirectory).getRepository(); } else { Log.logInfoRB("GIT_START", "clone"); CloneCommand c = Git.cloneRepository(); c.setURI(repositoryURL); c.setDirectory(localDirectory); try { c.call(); } catch (InvalidRemoteException e) { if (localDirectory.exists()) { deleteDirectory(localDirectory); } Throwable cause = e.getCause(); if (cause != null && cause instanceof org.eclipse.jgit.errors.NoRemoteRepositoryException) { BadRepositoryException bre = new BadRepositoryException( ((org.eclipse.jgit.errors.NoRemoteRepositoryException) cause) .getLocalizedMessage()); bre.initCause(e); throw bre; } throw e; } repository = Git.open(localDirectory).getRepository(); try (Git git = new Git(repository)) { git.submoduleInit().call(); git.submoduleUpdate().call(); } // Deal with line endings. A normalized repo has LF line endings. // OmegaT uses line endings of OS for storing tmx files. // To do auto converting, we need to change a setting: StoredConfig config = repository.getConfig(); if ("\r\n".equals(System.lineSeparator())) { // on windows machines, convert text files to CRLF config.setBoolean("core", null, "autocrlf", true); } else { // on Linux/Mac machines (using LF), don't convert text files // but use input format, unchanged. // NB: I don't know correct setting for OS'es like MacOS <= 9, // which uses CR. Git manual only speaks about converting from/to // CRLF, so for CR, you probably don't want conversion either. config.setString("core", null, "autocrlf", "input"); } config.save(); Log.logInfoRB("GIT_FINISH", "clone"); } // cleanup repository try (Git git = new Git(repository)) { git.reset().setMode(ResetType.HARD).call(); } } @Override public String getFileVersion(String file) throws Exception { File f = new File(localDirectory, file); if (!f.exists()) { return null; } return getCurrentVersion(); } protected String getCurrentVersion() throws Exception { try (RevWalk walk = new RevWalk(repository)) { Ref localBranch = repository.findRef("HEAD"); RevCommit headCommit = walk.lookupCommit(localBranch.getObjectId()); return headCommit.getName(); } } @Override public void switchToVersion(String version) throws Exception { try (Git git = new Git(repository)) { if (version == null) { version = REMOTE_BRANCH; // TODO fetch git.fetch().setRemote(REMOTE).call(); } Log.logDebug(LOGGER, "GIT switchToVersion {0} ", version); git.reset().setMode(ResetType.HARD).call(); git.checkout().setName(version).call(); git.branchDelete().setForce(true).setBranchNames(LOCAL_BRANCH).call(); git.checkout().setCreateBranch(true).setName(LOCAL_BRANCH).setStartPoint(version).call(); } } @Override public void addForCommit(String path) throws Exception { Log.logInfoRB("GIT_START", "addForCommit"); try (Git git = new Git(repository)) { git.add().addFilepattern(path).call(); Log.logInfoRB("GIT_FINISH", "addForCommit"); } catch (Exception ex) { Log.logErrorRB("GIT_ERROR", "addForCommit", ex.getMessage()); throw ex; } } @Override public String commit(String[] onVersions, String comment) throws Exception { if (onVersions != null) { // check versions String currentVersion = getCurrentVersion(); boolean hasVersion = false; for (String v : onVersions) { if (v != null) { hasVersion = true; break; } } if (hasVersion) { boolean found = false; for (String v : onVersions) { if (v != null) { if (v.equals(currentVersion)) { found = true; break; } } } if (!found) { throw new RuntimeException( "Version changed from " + Arrays.toString(onVersions) + " to " + currentVersion); } } } if (indexIsEmpty(DirCache.read(repository))) { // Nothing was actually added to the index so we can just return. Log.logInfoRB("GIT_NO_CHANGES", "upload"); return null; } Log.logInfoRB("GIT_START", "upload"); try (Git git = new Git(repository)) { RevCommit commit = git.commit().setMessage(comment).call(); Iterable<PushResult> results = git.push().setRemote(REMOTE).add(LOCAL_BRANCH) .call(); List<Status> statuses = StreamSupport.stream(results.spliterator(), false) .flatMap(r -> r.getRemoteUpdates().stream()).map(RemoteRefUpdate::getStatus) .collect(Collectors.toList()); String result; if (statuses.isEmpty() || statuses.stream().anyMatch(s -> s != RemoteRefUpdate.Status.OK)) { Log.logWarningRB("GIT_CONFLICT"); result = null; } else { result = commit.getName(); } Log.logDebug(LOGGER, "GIT committed into new version {0} ", result); Log.logInfoRB("GIT_FINISH", "upload"); return result; } catch (Exception ex) { Log.logErrorRB("GIT_ERROR", "upload", ex.getMessage()); if (ex instanceof TransportException) { throw new NetworkException(ex); } else { throw ex; } } } private boolean indexIsEmpty(DirCache dc) throws Exception { DirCacheIterator dci = new DirCacheIterator(dc); AbstractTreeIterator old = prepareTreeParser(repository, repository.resolve(Constants.HEAD)); try (Git git = new Git(repository)) { List<DiffEntry> diffs = git.diff().setOldTree(old).setNewTree(dci).call(); return diffs.isEmpty(); } } private static AbstractTreeIterator prepareTreeParser(Repository repository, ObjectId objId) throws Exception { // from the commit we can build the tree which allows us to construct // the TreeParser try (RevWalk walk = new RevWalk(repository)) { RevCommit commit = walk.parseCommit(objId); RevTree tree = walk.parseTree(commit.getTree().getId()); CanonicalTreeParser treeParser = new CanonicalTreeParser(); ObjectReader reader = repository.newObjectReader(); treeParser.reset(reader, tree.getId()); return treeParser; } } static public boolean deleteDirectory(File path) { if (path.exists()) { File[] files = path.listFiles(); for (int i = 0; i < files.length; i++) { if (files[i].isDirectory()) { deleteDirectory(files[i]); } else { files[i].delete(); } } } return (path.delete()); } /** * Determines whether or not the supplied URL represents a valid Git repository. * * <p> * Does the equivalent of <code>git ls-remote <i>url</i></code>. * * @param url * URL of supposed remote repository * @return true if repository appears to be valid, false otherwise */ public static boolean isGitRepository(String url) { // Heuristics to save some waiting time try { Collection<Ref> result = new LsRemoteCommand(null).setRemote(url).call(); return !result.isEmpty(); } catch (TransportException ex) { String message = ex.getMessage(); if (message.endsWith("not authorized") || message.endsWith("Auth fail") || message.contains("Too many authentication failures") || message.contains("Authentication is required")) { return true; } return false; } catch (GitAPIException ex) { return false; } catch (JGitInternalException ex) { // Happens if the URL is a Subversion URL like svn://... return false; } } }