/* license-start * * Copyright (C) 2008 - 2013 Crispico, <http://www.crispico.com/>. * * This program 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 version 3. * * This program 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, at <http://www.gnu.org/licenses/>. * * Contributors: * Crispico - Initial API and implementation * * license-end */ package org.flowerplatform.web.git; import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchEvent.Kind; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; import org.eclipse.core.resources.IProjectDescription; import org.eclipse.jgit.api.GitCommand; import org.eclipse.jgit.api.MergeResult; import org.eclipse.jgit.api.errors.DetachedHeadException; import org.eclipse.jgit.api.errors.InvalidConfigurationException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.NoHeadException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryCache; import org.eclipse.jgit.lib.RepositoryCache.FileKey; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.FetchResult; import org.eclipse.jgit.transport.PushResult; import org.eclipse.jgit.transport.RemoteRefUpdate; import org.eclipse.jgit.transport.TrackingRefUpdate; import org.eclipse.jgit.util.FS; import org.eclipse.osgi.framework.internal.core.FrameworkProperties; import org.flowerplatform.common.CommonPlugin; import org.flowerplatform.common.FlowerProperties; import org.flowerplatform.communication.CommunicationPlugin; import org.flowerplatform.communication.stateful_service.NamedLockPool; import org.flowerplatform.web.entity.User; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Cristina Constantienscu */ public class GitUtils { private static Logger logger = LoggerFactory.getLogger(GitUtils.class); public static final String MAIN_REPOSITORY = "main"; public static final String WORKING_DIRECTORY_PREFIX = "wd_"; public static final String GIT_REPOSITORIES_NAME = ".git-repositories"; /** * Flower Web Property. * @see /META-INF/flower-web.properties */ public static final String GIT_INSTALL_DIR = "git.git-install-dir"; /** * Name of the command file used to create a virtual repository on Windows. * @see /META-INF/git/git-new-workdir_win.cmd */ private static final String GIT_NEW_WORKDIR_WIN = "git-new-workdir_win.cmd"; /** * Name of the command file used to create a virtual repository on Linux. * @see /META-INF/git/git-new-workdir_linux */ private static final String GIT_NEW_WORKDIR_LINUX = "git-new-workdir_linux.sh"; static { FlowerProperties.AddProperty addProperty = new FlowerProperties.AddProperty(GIT_INSTALL_DIR, "") { /** * Verify if git.exe exists at given location. */ @Override protected String validateProperty(String input) { String git = CommonPlugin.getInstance().getFlowerProperties().getProperty(GIT_INSTALL_DIR) + "/cmd/git.exe"; if (!new File(git).exists()) { return String.format("Git executable wasn't found at '%s'! Please verify '%s' property!", git, GIT_INSTALL_DIR); } return null; } }.setInputFromFileMandatory(System.getProperty("os.name").toLowerCase().indexOf("win") >= 0); CommonPlugin.getInstance().getFlowerProperties().addProperty(addProperty); // verify JavaVM version; it must be >= 1.7 String jvmVersion = System.getProperty("java.vm.specification.version"); if (jvmVersion.compareTo("1.7") < 0) { logger.error("Your current JVM version is {}. In order to use Git properly, the JVM version needs to be at least 1.7!", jvmVersion); } /* * Each repository configures this property at creation: * core.fileMode * If false, the executable bit differences between the index and the working copy are ignored; * useful on broken filesystems like FAT. See git-update-index(1). * * The default is true, except git-clone(1) or git-init(1) will probe and set core.fileMode false if appropriate when the repository is created. * * We set it always to false, because we don't want to add "execute" permission on files. */ try { Class<?> fsPosixJava6 = Class.forName("org.eclipse.jgit.util.FS_POSIX_Java6"); if (fsPosixJava6.isInstance(FS.DETECTED)) { Field field = fsPosixJava6.getDeclaredField("canExecute"); field.setAccessible(true); // remove final modifier from field Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); field.set(FS.DETECTED, null); field = fsPosixJava6.getDeclaredField("setExecute"); field.setAccessible(true); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); field.set(FS.DETECTED, null); field = FS.class.getDeclaredField("DETECTED"); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); field.set(FS.DETECTED, FS.detect()); } } catch (Exception e) { throw new RuntimeException("Exception thrown while setting 'non-executable' state for git repository files!", e); } } public File getGitRepositoriesFile(File orgFile) { return new File(CommonPlugin.getInstance().getWorkspaceRoot(), orgFile.getName() + "/" + GIT_REPOSITORIES_NAME + "/"); } public Repository getRepository(File repoFile) { File gitDir = getGitDir(repoFile); if (gitDir != null) { try { Repository repository = RepositoryCache.open(FileKey.exact(gitDir, FS.DETECTED)); return repository; } catch (IOException e) { // TODO CC: log } } return null; } public File getMainRepositoryFile(File repoFile, boolean createIfNecessary) { File mainRepoFile = new File(repoFile, MAIN_REPOSITORY); if (createIfNecessary && !mainRepoFile.exists()) { if (!mainRepoFile.mkdirs()) { logger.error("Cannot create main repository file for {}", repoFile); return null; } } return mainRepoFile; } public Repository getMainRepository(File repoFile) { return getRepository(getMainRepositoryFile(repoFile, false)); } public File getGitDir(File file) { if (file.exists()) { while (file != null) { if (GIT_REPOSITORIES_NAME.equals(file.getName())) { return null; } if (CommonPlugin.getInstance().getWorkspaceRoot().getName().equals(file.getName())) { return null; } if (RepositoryCache.FileKey.isGitRepository(file, FS.DETECTED)) { return file; } else if (RepositoryCache.FileKey.isGitRepository(new File(file, Constants.DOT_GIT), FS.DETECTED)) { return new File(file, Constants.DOT_GIT); } file = file.getParentFile(); } } return null; } public boolean isRepository(File file) { return getGitDir(file) != null; } public String getRepositoryName(Repository repo) { return repo.getDirectory().getParentFile().getParentFile().getName(); } /** * Deletes the given file and its content. * <p> * If the file is a symbolic link, deletes only the file. * Otherwise, deletes also the content from the original location * (file.listFiles() returns the children files from original location). */ public void delete(File f) { if (f.isDirectory() && !Files.isSymbolicLink(Paths.get(f.toURI()))) { for (File c : f.listFiles()) { delete(c); } } f.delete(); } public boolean isAuthentificationException(Exception e) { TransportException cause = null; if (e.getCause() instanceof TransportException) { cause = (TransportException) e.getCause(); } else if (e instanceof TransportException) { cause = (TransportException) e; } if (cause != null && (matchMessage(JGitText.get().notAuthorized, cause.getMessage()) || cause.getMessage().endsWith("username must not be null.") || cause.getMessage().endsWith("host must not be null."))) { return true; } return false; } /** * Creates a string message to be displayed on client side * to inform the user about push result operation. */ public String handlePushResult(PushResult pushResult) { StringBuilder sb = new StringBuilder(); sb.append(pushResult.getMessages()); sb.append("\n"); for (RemoteRefUpdate rru : pushResult.getRemoteUpdates()) { String rm = rru.getRemoteName(); RemoteRefUpdate.Status status = rru.getStatus(); sb.append(rm); sb.append(" -> "); sb.append(status.name()); sb.append("\n"); } return sb.toString(); } public String handleMergeResult(MergeResult mergeResult) { StringBuilder sb = new StringBuilder(); if (mergeResult == null) { return sb.toString(); } sb.append("Status: "); sb.append(mergeResult.getMergeStatus()); sb.append("\n"); if (mergeResult.getMergedCommits() != null) { sb.append("\nMerged commits: "); sb.append("\n"); for (ObjectId id : mergeResult.getMergedCommits()) { sb.append(id.getName()); sb.append("\n"); } } if (mergeResult.getCheckoutConflicts() != null) { sb.append("\nConflicts: "); sb.append("\n"); for (String conflict : mergeResult.getCheckoutConflicts()) { sb.append(conflict); sb.append("\n"); } } if (mergeResult.getFailingPaths() != null) { sb.append("\nFailing paths: "); sb.append("\n"); for (String path : mergeResult.getFailingPaths().keySet()) { sb.append(path); sb.append(" -> "); sb.append(mergeResult.getFailingPaths().get(path).toString()); sb.append("\n"); } } return sb.toString(); } public boolean matchMessage(String pattern, String message) { if (message == null) { return false; } int argsNum = 0; for (int i = 0; i < pattern.length(); i++) { if (pattern.charAt(i) == '{') { argsNum++; } } Object[] args = new Object[argsNum]; for (int i = 0; i < args.length; i++) { args[i] = ".*"; //$NON-NLS-1$ } return Pattern.matches(".*" + MessageFormat.format(pattern, args) + ".*", message); //$NON-NLS-1$ //$NON-NLS-2$ } /** * Creates a string message to be displayed on client side * to inform the user about fetch result operation. */ public String handleFetchResult(FetchResult fetchResult) { StringBuilder sb = new StringBuilder(); if (fetchResult.getTrackingRefUpdates().size() > 0) { // handle result for (TrackingRefUpdate updateRes : fetchResult.getTrackingRefUpdates()) { sb.append(updateRes.getRemoteName()); sb.append(" -> "); sb.append(updateRes.getLocalName()); sb.append(" "); sb.append(updateRes.getOldObjectId() == null ? "" : updateRes.getOldObjectId().abbreviate(7).name()); sb.append(".."); sb.append(updateRes.getNewObjectId() == null ? "" : updateRes.getNewObjectId().abbreviate(7).name()); sb.append(" "); Result res = updateRes.getResult(); switch (res) { case NOT_ATTEMPTED : case NO_CHANGE : case NEW : case FORCED : case FAST_FORWARD : case RENAMED : sb.append("OK."); break; case REJECTED : sb.append("Fetch rejected, not a fast-forward."); case REJECTED_CURRENT_BRANCH : sb.append("Rejected because trying to delete the current branch."); default : sb.append(res.name()); } sb.append("\n"); } } else { sb.append("OK."); } return sb.toString(); } public boolean findProjectFiles(final Collection<File> files, File directory, Set<String> visistedDirs) { if (directory == null) return false; if (directory.getName().equals(Constants.DOT_GIT) && FileKey.isGitRepository(directory, FS.DETECTED)) { return false; } File[] contents = directory.listFiles(); if (contents == null || contents.length == 0) { return false; } Set<String> directoriesVisited; // Initialize recursion guard for recursive symbolic links if (visistedDirs == null) { directoriesVisited = new HashSet<String>(); try { directoriesVisited.add(directory.getCanonicalPath()); } catch (IOException exception) { return false; } } else { directoriesVisited = visistedDirs; } // first look for project description files String dotProject = IProjectDescription.DESCRIPTION_FILE_NAME; for (int i = 0; i < contents.length; i++) { File file = contents[i]; if (file.isFile() && file.getName().equals(dotProject) && !files.contains(file)) { files.add(file); } } // recurse into sub-directories (even when project was found above, for nested projects) for (int i = 0; i < contents.length; i++) { // Skip non-directories if (!contents[i].isDirectory()) { continue; } // Skip .metadata folders if (contents[i].getName().equals(".metadata")) { continue; } try { String canonicalPath = contents[i].getCanonicalPath(); if (!directoriesVisited.add(canonicalPath)) { // already been here --> do not recurse continue; } } catch (IOException exception) { return false; } findProjectFiles(files, contents[i], directoriesVisited); } return true; } public RevCommit getHeadCommit(Repository repository) { RevCommit headCommit = null; try { ObjectId parentId = repository.resolve(Constants.HEAD); if (parentId != null) { headCommit = new RevWalk(repository).parseCommit(parentId); } } catch (IOException e) { return null; } return headCommit; } @SuppressWarnings("restriction") public Object[] getFetchPushUpstreamDataRefSpecAndRemote(Repository repository) throws InvalidConfigurationException, NoHeadException, DetachedHeadException { String branchName; String fullBranch; try { fullBranch = repository.getFullBranch(); if (fullBranch == null) { throw new NoHeadException(JGitText.get().pullOnRepoWithoutHEADCurrentlyNotSupported); } if (!fullBranch.startsWith(Constants.R_HEADS)) { // we can not pull if HEAD is detached and branch is not // specified explicitly throw new DetachedHeadException(); } branchName = fullBranch.substring(Constants.R_HEADS.length()); } catch (IOException e) { throw new JGitInternalException(JGitText.get().exceptionCaughtDuringExecutionOfPullCommand, e); } // get the configured remote for the currently checked out branch // stored in configuration key branch.<branch name>.remote Config repoConfig = repository.getConfig(); String remote = repoConfig.getString( ConfigConstants.CONFIG_BRANCH_SECTION, branchName, ConfigConstants.CONFIG_KEY_REMOTE); if (remote == null) { // fall back to default remote remote = Constants.DEFAULT_REMOTE_NAME; } // get the name of the branch in the remote repository // stored in configuration key branch.<branch name>.merge String remoteBranchName = repoConfig.getString( ConfigConstants.CONFIG_BRANCH_SECTION, branchName, ConfigConstants.CONFIG_KEY_MERGE); if (remoteBranchName == null) { String missingKey = ConfigConstants.CONFIG_BRANCH_SECTION + "." + branchName + "." + ConfigConstants.CONFIG_KEY_MERGE; throw new InvalidConfigurationException(MessageFormat.format( JGitText.get().missingConfigurationForKey, missingKey)); } // check if the branch is configured for pull-rebase boolean doRebase = repoConfig.getBoolean( ConfigConstants.CONFIG_BRANCH_SECTION, branchName, ConfigConstants.CONFIG_KEY_REBASE, false); final boolean isRemote = !remote.equals("."); //$NON-NLS-1$ String remoteUri; if (isRemote) { remoteUri = repoConfig.getString( ConfigConstants.CONFIG_REMOTE_SECTION, remote, ConfigConstants.CONFIG_KEY_URL); if (remoteUri == null) { String missingKey = ConfigConstants.CONFIG_REMOTE_SECTION + "." + remote + "." + ConfigConstants.CONFIG_KEY_URL; throw new InvalidConfigurationException(MessageFormat.format( JGitText.get().missingConfigurationForKey, missingKey)); } return new Object[] {fullBranch, remoteBranchName, remoteUri, doRebase}; } return null; } public void listenForChanges(File file) throws IOException { Path path = file.toPath(); if (file.isDirectory()) { WatchService ws = path.getFileSystem().newWatchService(); path.register(ws, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); WatchKey watch = null; while (true) { System.out.println("Watching directory: " + file.getPath()); try { watch = ws.take(); } catch (InterruptedException ex) { System.err.println("Interrupted"); } List<WatchEvent<?>> events = watch.pollEvents(); if (!watch.reset()) { break; } for (WatchEvent<?> event : events) { Kind<Path> kind = (Kind<Path>) event.kind(); Path context = (Path) event.context(); if (kind.equals(StandardWatchEventKinds.OVERFLOW)) { System.out.println("OVERFLOW"); } else if (kind.equals(StandardWatchEventKinds.ENTRY_CREATE)) { System.out.println("Created: " + context.getFileName()); } else if (kind.equals(StandardWatchEventKinds.ENTRY_DELETE)) { System.out.println("Deleted: " + context.getFileName()); } else if (kind.equals(StandardWatchEventKinds.ENTRY_MODIFY)) { System.out.println("Modified: " + context.getFileName()); } } } } else { System.err.println("Not a directory. Will exit."); } } private NamedLockPool namedLockPool = new NamedLockPool(); /** * This method must be used to set user configuration before running * some GIT commands that uses it. * * <p> * A lock/unlock on repository is done before/after the command is executed * because the configuration modifies the same file and this will not be * thread safe any more. */ public Object runGitCommandInUserRepoConfig(Repository repo, GitCommand<?> command) throws Exception { namedLockPool.lock(repo.getDirectory().getPath()); try { StoredConfig c = repo.getConfig(); c.load(); User user = (User) CommunicationPlugin.tlCurrentPrincipal.get().getUser(); c.setString(ConfigConstants.CONFIG_USER_SECTION, null, ConfigConstants.CONFIG_KEY_NAME, user.getName()); c.setString(ConfigConstants.CONFIG_USER_SECTION, null, ConfigConstants.CONFIG_KEY_EMAIL, user.getEmail()); c.save(); return command.call(); } catch (Exception e) { throw e; } finally { namedLockPool.unlock(repo.getDirectory().getPath()); } } /** * Executes the corresponding Windows/Linux script to create a virtual repository. */ @SuppressWarnings("restriction") public String run_git_workdir_cmd(String source, String destination) { File file = null; try { String OS = System.getProperty("os.name").toLowerCase(); boolean isWindows = true; if (OS.indexOf("nix") >= 0 || OS.indexOf("nux") >= 0 || OS.indexOf("aix") > 0) { isWindows = false; } else if (!(OS.indexOf("win") >= 0)) { return "git-new-workdir command only supports format for Windows/Linux!"; } String cmdName = isWindows ? GIT_NEW_WORKDIR_WIN : GIT_NEW_WORKDIR_LINUX; file = File.createTempFile("git", isWindows ? ".cmd" : ".sh", new File(FrameworkProperties.getProperty("flower.server.tmpdir"))); InputStream is = getClass().getClassLoader().getResourceAsStream("META-INF/git/" + cmdName); OutputStream out = new FileOutputStream(file); IOUtils.copy(is, out); is.close(); out.close(); if (!file.exists()) { return String.format("%s wasn't found at '%s'!", cmdName, file.getAbsolutePath()); } file.setExecutable(true); List<String> cmd = new ArrayList<String>(); cmd.add(file.getAbsolutePath()); cmd.add(source); cmd.add(destination); if (isWindows) { String git = CommonPlugin.getInstance().getFlowerProperties().getProperty(GIT_INSTALL_DIR) + "/cmd/git.exe"; if (!new File(git).exists()) { return String.format("Git executable wasn't found at '%s'! Please verify '%s' property!", git, GIT_INSTALL_DIR); } cmd.add(git); } Process proc = Runtime.getRuntime().exec(cmd.toArray(new String[cmd.size()])); if (logger.isDebugEnabled()) { // any error message? StreamGobbler errorGobbler = new StreamGobbler(proc.getErrorStream(), "ERROR"); errorGobbler.start(); // any output? StreamGobbler outputGobbler = new StreamGobbler(proc.getInputStream(), "OUTPUT"); outputGobbler.start(); } // any error??? int exitVal = proc.waitFor(); switch (exitVal) { case 0: return null; // OK case 1: return String.format("Usage: %s ^<repository^> ^<new_workdir^> %s[^<branch^>]", cmdName, isWindows ? "^<git_exe_location^> " : ""); case 2: return String.format("Directory not found: '%s'!", source); case 3: return String.format("Not a git repository: '%s'!", source); case 4: return String.format("'%s' is a bare repository!", source); case 5: return String.format("Destination directory '%s' already exists!", destination); case 6: return String.format("Unable to create '%s'!", destination); } } catch (Exception e) { logger.error("Exception thrown while running git-new-workdir command!", e); return "Exception thrown while creating working directory!"; } finally { if (file != null) { file.delete(); } } return null; } /** * Class used to get the output data while executing a runtime process. * * @see GitService#run_git_workdir_cmd(String, String) */ class StreamGobbler extends Thread { private InputStream is; private String type; private String message; public StreamGobbler(InputStream is, String type) { this.is = is; this.type = type; } public String getMessage() { return message; } public void run() { try { InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); String line = null; while ((line = br.readLine()) != null) { logger.debug(type + ">" + line); } } catch (IOException ioe) { logger.error("Exception thrown while writing command line text", ioe); } } } }