org.apache.maven.scm.provider.git.gitexe.command.status.GitStatusConsumer.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.maven.scm.provider.git.gitexe.command.status.GitStatusConsumer.java

Source

package org.apache.maven.scm.provider.git.gitexe.command.status;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringUtils;
import org.apache.maven.scm.ScmFile;
import org.apache.maven.scm.ScmFileStatus;
import org.apache.maven.scm.log.ScmLogger;
import org.codehaus.plexus.util.cli.StreamConsumer;

/**
 * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
 */
public class GitStatusConsumer implements StreamConsumer {

    /**
     * The pattern used to match added file lines
     */
    private static final Pattern ADDED_PATTERN = Pattern.compile("^A[ M]* (.*)$");

    /**
     * The pattern used to match modified file lines
     */
    private static final Pattern MODIFIED_PATTERN = Pattern.compile("^ *M[ M]* (.*)$");

    /**
     * The pattern used to match deleted file lines
     */
    private static final Pattern DELETED_PATTERN = Pattern.compile("^ *D * (.*)$");

    /**
     * The pattern used to match renamed file lines
     */
    private static final Pattern RENAMED_PATTERN = Pattern.compile("^R  (.*) -> (.*)$");

    private ScmLogger logger;

    private File workingDirectory;

    /**
     * Entries are relative to working directory, not to the repositoryroot
     */
    private List<ScmFile> changedFiles = new ArrayList<ScmFile>();

    private URI relativeRepositoryPath;

    // ----------------------------------------------------------------------
    //
    // ----------------------------------------------------------------------

    /**
     * Consumer when workingDirectory and repositoryRootDirectory are the same
     * 
     * @param logger the logger
     * @param workingDirectory the working directory
     */
    public GitStatusConsumer(ScmLogger logger, File workingDirectory) {
        this.logger = logger;
        this.workingDirectory = workingDirectory;
    }

    /**
     * Assuming that you have to discover the repositoryRoot, this is how you can get the
     * <code>relativeRepositoryPath</code>
     * <pre>
     * URI.create( repositoryRoot ).relativize( fileSet.getBasedir().toURI() )
     * </pre>
     * 
     * @param logger the logger
     * @param workingDirectory the working directory
     * @param relativeRepositoryPath the working directory relative to the repository root
     * @since 1.9
     * @see GitStatusCommand#createRevparseShowToplevelCommand(org.apache.maven.scm.ScmFileSet)
     */
    public GitStatusConsumer(ScmLogger logger, File workingDirectory, URI relativeRepositoryPath) {
        this(logger, workingDirectory);
        this.relativeRepositoryPath = relativeRepositoryPath;
    }

    // ----------------------------------------------------------------------
    // StreamConsumer Implementation
    // ----------------------------------------------------------------------

    /**
     * {@inheritDoc}
     */
    public void consumeLine(String line) {
        if (logger.isDebugEnabled()) {
            logger.debug(line);
        }
        if (StringUtils.isEmpty(line)) {
            return;
        }

        ScmFileStatus status = null;

        List<String> files = new ArrayList<String>();

        Matcher matcher;
        if ((matcher = ADDED_PATTERN.matcher(line)).find()) {
            status = ScmFileStatus.ADDED;
            files.add(resolvePath(matcher.group(1), relativeRepositoryPath));
        } else if ((matcher = MODIFIED_PATTERN.matcher(line)).find()) {
            status = ScmFileStatus.MODIFIED;
            files.add(resolvePath(matcher.group(1), relativeRepositoryPath));
        } else if ((matcher = DELETED_PATTERN.matcher(line)).find()) {
            status = ScmFileStatus.DELETED;
            files.add(resolvePath(matcher.group(1), relativeRepositoryPath));
        } else if ((matcher = RENAMED_PATTERN.matcher(line)).find()) {
            status = ScmFileStatus.RENAMED;
            files.add(resolvePath(matcher.group(1), relativeRepositoryPath));
            files.add(resolvePath(matcher.group(2), relativeRepositoryPath));
            logger.debug("RENAMED status for line '" + line + "' files added '" + matcher.group(1) + "' '"
                    + matcher.group(2));
        } else {
            logger.warn("Ignoring unrecognized line: " + line);
            return;
        }

        // If the file isn't a file; don't add it.
        if (!files.isEmpty() && status != null) {
            if (workingDirectory != null) {
                if (status == ScmFileStatus.RENAMED) {
                    String oldFilePath = files.get(0);
                    String newFilePath = files.get(1);
                    if (isFile(oldFilePath)) {
                        logger.debug("file '" + oldFilePath + "' is a file");
                        return;
                    } else {
                        logger.debug("file '" + oldFilePath + "' not a file");
                    }
                    if (!isFile(newFilePath)) {
                        logger.debug("file '" + newFilePath + "' not a file");
                        return;
                    } else {
                        logger.debug("file '" + newFilePath + "' is a file");
                    }
                } else if (status == ScmFileStatus.DELETED) {
                    if (isFile(files.get(0))) {
                        return;
                    }
                } else {
                    if (!isFile(files.get(0))) {
                        return;
                    }
                }
            }

            for (String file : files) {
                changedFiles.add(new ScmFile(file, status));
            }
        }
    }

    private boolean isFile(String file) {
        File targetFile;
        if (relativeRepositoryPath == null) {
            targetFile = new File(workingDirectory, file);
        } else {
            targetFile = new File(relativeRepositoryPath.getPath(), file);
        }
        return targetFile.isFile();
    }

    protected static String resolvePath(String fileEntry, URI path) {
        /* Quotes may be included (from the git status line) when an fileEntry includes spaces */
        String cleanedEntry = stripQuotes(fileEntry);
        if (path != null) {
            return resolveURI(cleanedEntry, path).getPath();
        } else {
            return cleanedEntry;
        }
    }

    /**
     * 
     * @param fileEntry the fileEntry, must not be {@code null}
     * @param path the path, must not be {@code null}
     * @return
     */
    public static URI resolveURI(String fileEntry, URI path) {
        // When using URI.create, spaces need to be escaped but not the slashes, so we can't use
        // URLEncoder.encode( String, String )
        // new File( String ).toURI() results in an absolute URI while path is relative, so that can't be used either.
        return path.relativize(URI.create(stripQuotes(fileEntry).replace(" ", "%20")));
    }

    public List<ScmFile> getChangedFiles() {
        return changedFiles;
    }

    /**
     * @param str the (potentially quoted) string, must not be {@code null}
     * @return the string with a pair of double quotes removed (if they existed)
     */
    private static String stripQuotes(String str) {
        int strLen = str.length();
        return (strLen > 0 && str.startsWith("\"") && str.endsWith("\"")) ? unescape(str.substring(1, strLen - 1))
                : str;
    }

    /**
     * Dequote a quoted string generated by git status --porcelain.
     * The leading and trailing quotes have already been removed. 
     * @param fileEntry
     * @return
     */
    private static String unescape(String fileEntry) {
        // If there are no escaped characters, just return the input argument
        int pos = fileEntry.indexOf('\\');
        if (pos == -1) {
            return fileEntry;
        }

        // We have escaped characters
        byte[] inba = fileEntry.getBytes();
        int inSub = 0; // Input subscript into fileEntry
        byte[] outba = new byte[fileEntry.length()];
        int outSub = 0; // Output subscript into outba

        while (true) {
            System.arraycopy(inba, inSub, outba, outSub, pos - inSub);
            outSub += pos - inSub;
            inSub = pos + 1;
            switch ((char) inba[inSub++]) {
            case '"':
                outba[outSub++] = '"';
                break;

            case 'a':
                outba[outSub++] = 7; // Bell
                break;

            case 'b':
                outba[outSub++] = '\b';
                break;

            case 't':
                outba[outSub++] = '\t';
                break;

            case 'n':
                outba[outSub++] = '\n';
                break;

            case 'v':
                outba[outSub++] = 11; // Vertical tab
                break;

            case 'f':
                outba[outSub++] = '\f';
                break;

            case 'r':
                outba[outSub++] = '\f';
                break;

            case '\\':
                outba[outSub++] = '\\';
                break;

            case '0':
            case '1':
            case '2':
            case '3':
                // This assumes that the octal escape here is valid.
                byte b = (byte) ((inba[inSub - 1] - '0') << 6);
                b |= (byte) ((inba[inSub++] - '0') << 3);
                b |= (byte) (inba[inSub++] - '0');
                outba[outSub++] = b;
                break;

            default:
                //This is an invalid escape in a string.  Just copy it.
                outba[outSub++] = '\\';
                inSub--;
                break;
            }
            pos = fileEntry.indexOf('\\', inSub);
            if (pos == -1) // No more backslashes; we're done
            {
                System.arraycopy(inba, inSub, outba, outSub, inba.length - inSub);
                outSub += inba.length - inSub;
                break;
            }
        }
        try {
            // explicit say UTF-8, otherwise it'll fail at least on Windows cmdline
            return new String(outba, 0, outSub, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }
}