org.eclipse.jgit.patch.FileHeader.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.jgit.patch.FileHeader.java

Source

/*
 * Copyright (C) 2008-2009, Google Inc.
 * and other copyright owners as documented in the project's IP log.
 *
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Distribution License v1.0 which
 * accompanies this distribution, is reproduced below, and is
 * available at http://www.eclipse.org/org/documents/edl-v10.php
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials provided
 *   with the distribution.
 *
 * - Neither the name of the Eclipse Foundation, Inc. nor the
 *   names of its contributors may be used to endorse or promote
 *   products derived from this software without specific prior
 *   written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.eclipse.jgit.patch;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.encodeASCII;
import static org.eclipse.jgit.util.RawParseUtils.decode;
import static org.eclipse.jgit.util.RawParseUtils.decodeNoFallback;
import static org.eclipse.jgit.util.RawParseUtils.extractBinaryString;
import static org.eclipse.jgit.util.RawParseUtils.match;
import static org.eclipse.jgit.util.RawParseUtils.nextLF;
import static org.eclipse.jgit.util.RawParseUtils.parseBase10;

import java.io.IOException;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.EditList;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.util.QuotedString;
import org.eclipse.jgit.util.RawParseUtils;
import org.eclipse.jgit.util.TemporaryBuffer;

/**
 * Patch header describing an action for a single file path.
 */
public class FileHeader extends DiffEntry {
    private static final byte[] OLD_MODE = encodeASCII("old mode "); //$NON-NLS-1$

    private static final byte[] NEW_MODE = encodeASCII("new mode "); //$NON-NLS-1$

    static final byte[] DELETED_FILE_MODE = encodeASCII("deleted file mode "); //$NON-NLS-1$

    static final byte[] NEW_FILE_MODE = encodeASCII("new file mode "); //$NON-NLS-1$

    private static final byte[] COPY_FROM = encodeASCII("copy from "); //$NON-NLS-1$

    private static final byte[] COPY_TO = encodeASCII("copy to "); //$NON-NLS-1$

    private static final byte[] RENAME_OLD = encodeASCII("rename old "); //$NON-NLS-1$

    private static final byte[] RENAME_NEW = encodeASCII("rename new "); //$NON-NLS-1$

    private static final byte[] RENAME_FROM = encodeASCII("rename from "); //$NON-NLS-1$

    private static final byte[] RENAME_TO = encodeASCII("rename to "); //$NON-NLS-1$

    private static final byte[] SIMILARITY_INDEX = encodeASCII("similarity index "); //$NON-NLS-1$

    private static final byte[] DISSIMILARITY_INDEX = encodeASCII("dissimilarity index "); //$NON-NLS-1$

    static final byte[] INDEX = encodeASCII("index "); //$NON-NLS-1$

    static final byte[] OLD_NAME = encodeASCII("--- "); //$NON-NLS-1$

    static final byte[] NEW_NAME = encodeASCII("+++ "); //$NON-NLS-1$

    /** Type of patch used by this file. */
    public static enum PatchType {
        /** A traditional unified diff style patch of a text file. */
        UNIFIED,

        /** An empty patch with a message "Binary files ... differ" */
        BINARY,

        /** A Git binary patch, holding pre and post image deltas */
        GIT_BINARY;
    }

    /** Buffer holding the patch data for this file. */
    final byte[] buf;

    /** Offset within {@link #buf} to the "diff ..." line. */
    final int startOffset;

    /** Position 1 past the end of this file within {@link #buf}. */
    int endOffset;

    /** Type of patch used to modify this file */
    PatchType patchType;

    /** The hunks of this file */
    private List<HunkHeader> hunks;

    /** If {@link #patchType} is {@link PatchType#GIT_BINARY}, the new image */
    BinaryHunk forwardBinaryHunk;

    /** If {@link #patchType} is {@link PatchType#GIT_BINARY}, the old image */
    BinaryHunk reverseBinaryHunk;

    /**
     * Constructs a new FileHeader
     *
     * @param headerLines
     *            buffer holding the diff header for this file
     * @param edits
     *            the edits for this file
     * @param type
     *            the type of patch used to modify this file
     */
    public FileHeader(byte[] headerLines, EditList edits, PatchType type) {
        this(headerLines, 0);
        endOffset = headerLines.length;
        int ptr = parseGitFileName(Patch.DIFF_GIT.length, headerLines.length);
        parseGitHeaders(ptr, headerLines.length);
        this.patchType = type;
        addHunk(new HunkHeader(this, edits));
    }

    FileHeader(byte[] b, int offset) {
        buf = b;
        startOffset = offset;
        changeType = ChangeType.MODIFY; // unless otherwise designated
        patchType = PatchType.UNIFIED;
    }

    int getParentCount() {
        return 1;
    }

    /**
     * Get the byte array holding this file's patch script.
     *
     * @return the byte array holding this file's patch script.
     */
    public byte[] getBuffer() {
        return buf;
    }

    /**
     * Get offset of the start of this file's script in {@link #getBuffer()}.
     *
     * @return offset of the start of this file's script in
     *         {@link #getBuffer()}.
     */
    public int getStartOffset() {
        return startOffset;
    }

    /**
     * Get offset one past the end of the file script.
     *
     * @return offset one past the end of the file script.
     */
    public int getEndOffset() {
        return endOffset;
    }

    /**
     * Convert the patch script for this file into a string.
     * <p>
     * The default character encoding
     * ({@link java.nio.charset.StandardCharsets#UTF_8}) is assumed for both the
     * old and new files.
     *
     * @return the patch script, as a Unicode string.
     */
    public String getScriptText() {
        return getScriptText(null, null);
    }

    /**
     * Convert the patch script for this file into a string.
     *
     * @param oldCharset
     *            hint character set to decode the old lines with.
     * @param newCharset
     *            hint character set to decode the new lines with.
     * @return the patch script, as a Unicode string.
     */
    public String getScriptText(Charset oldCharset, Charset newCharset) {
        return getScriptText(new Charset[] { oldCharset, newCharset });
    }

    String getScriptText(Charset[] charsetGuess) {
        if (getHunks().isEmpty()) {
            // If we have no hunks then we can safely assume the entire
            // patch is a binary style patch, or a meta-data only style
            // patch. Either way the encoding of the headers should be
            // strictly 7-bit US-ASCII and the body is either 7-bit ASCII
            // (due to the base 85 encoding used for a BinaryHunk) or is
            // arbitrary noise we have chosen to ignore and not understand
            // (e.g. the message "Binary files ... differ").
            //
            return extractBinaryString(buf, startOffset, endOffset);
        }

        if (charsetGuess != null && charsetGuess.length != getParentCount() + 1)
            throw new IllegalArgumentException(MessageFormat.format(JGitText.get().expectedCharacterEncodingGuesses,
                    Integer.valueOf(getParentCount() + 1)));

        if (trySimpleConversion(charsetGuess)) {
            Charset cs = charsetGuess != null ? charsetGuess[0] : null;
            if (cs == null) {
                cs = UTF_8;
            }
            try {
                return decodeNoFallback(cs, buf, startOffset, endOffset);
            } catch (CharacterCodingException cee) {
                // Try the much slower, more-memory intensive version which
                // can handle a character set conversion patch.
            }
        }

        final StringBuilder r = new StringBuilder(endOffset - startOffset);

        // Always treat the headers as US-ASCII; Git file names are encoded
        // in a C style escape if any character has the high-bit set.
        //
        final int hdrEnd = getHunks().get(0).getStartOffset();
        for (int ptr = startOffset; ptr < hdrEnd;) {
            final int eol = Math.min(hdrEnd, nextLF(buf, ptr));
            r.append(extractBinaryString(buf, ptr, eol));
            ptr = eol;
        }

        final String[] files = extractFileLines(charsetGuess);
        final int[] offsets = new int[files.length];
        for (HunkHeader h : getHunks())
            h.extractFileLines(r, files, offsets);
        return r.toString();
    }

    private static boolean trySimpleConversion(Charset[] charsetGuess) {
        if (charsetGuess == null)
            return true;
        for (int i = 1; i < charsetGuess.length; i++) {
            if (charsetGuess[i] != charsetGuess[0])
                return false;
        }
        return true;
    }

    private String[] extractFileLines(Charset[] csGuess) {
        final TemporaryBuffer[] tmp = new TemporaryBuffer[getParentCount() + 1];
        try {
            for (int i = 0; i < tmp.length; i++)
                tmp[i] = new TemporaryBuffer.Heap(Integer.MAX_VALUE);
            for (HunkHeader h : getHunks())
                h.extractFileLines(tmp);

            final String[] r = new String[tmp.length];
            for (int i = 0; i < tmp.length; i++) {
                Charset cs = csGuess != null ? csGuess[i] : null;
                if (cs == null) {
                    cs = UTF_8;
                }
                r[i] = RawParseUtils.decode(cs, tmp[i].toByteArray());
            }
            return r;
        } catch (IOException ioe) {
            throw new RuntimeException(JGitText.get().cannotConvertScriptToText, ioe);
        }
    }

    /**
     * Get style of patch used to modify this file.
     *
     * @return style of patch used to modify this file.
     */
    public PatchType getPatchType() {
        return patchType;
    }

    /**
     * Whether this patch modifies metadata about a file
     *
     * @return {@code true} if this patch modifies metadata about a file .
     */
    public boolean hasMetaDataChanges() {
        return changeType != ChangeType.MODIFY || newMode != oldMode;
    }

    /**
     * Get hunks altering this file; in order of appearance in patch
     *
     * @return hunks altering this file; in order of appearance in patch.
     */
    public List<? extends HunkHeader> getHunks() {
        if (hunks == null)
            return Collections.emptyList();
        return hunks;
    }

    void addHunk(HunkHeader h) {
        if (h.getFileHeader() != this)
            throw new IllegalArgumentException(JGitText.get().hunkBelongsToAnotherFile);
        if (hunks == null)
            hunks = new ArrayList<>();
        hunks.add(h);
    }

    HunkHeader newHunkHeader(int offset) {
        return new HunkHeader(this, offset);
    }

    /**
     * Get the new-image delta/literal if this is a
     * {@link PatchType#GIT_BINARY}.
     *
     * @return the new-image delta/literal if this is a
     *         {@link PatchType#GIT_BINARY}.
     */
    public BinaryHunk getForwardBinaryHunk() {
        return forwardBinaryHunk;
    }

    /**
     * Get the old-image delta/literal if this is a
     * {@link PatchType#GIT_BINARY}.
     *
     * @return the old-image delta/literal if this is a
     *         {@link PatchType#GIT_BINARY}.
     */
    public BinaryHunk getReverseBinaryHunk() {
        return reverseBinaryHunk;
    }

    /**
     * Convert to a list describing the content edits performed on this file.
     *
     * @return a list describing the content edits performed on this file.
     */
    public EditList toEditList() {
        final EditList r = new EditList();
        for (HunkHeader hunk : hunks)
            r.addAll(hunk.toEditList());
        return r;
    }

    /**
     * Parse a "diff --git" or "diff --cc" line.
     *
     * @param ptr
     *            first character after the "diff --git " or "diff --cc " part.
     * @param end
     *            one past the last position to parse.
     * @return first character after the LF at the end of the line; -1 on error.
     */
    int parseGitFileName(int ptr, int end) {
        final int eol = nextLF(buf, ptr);
        final int bol = ptr;
        if (eol >= end) {
            return -1;
        }

        // buffer[ptr..eol] looks like "a/foo b/foo\n". After the first
        // A regex to match this is "^[^/]+/(.*?) [^/+]+/\1\n$". There
        // is only one way to split the line such that text to the left
        // of the space matches the text to the right, excluding the part
        // before the first slash.
        //

        final int aStart = nextLF(buf, ptr, '/');
        if (aStart >= eol)
            return eol;

        while (ptr < eol) {
            final int sp = nextLF(buf, ptr, ' ');
            if (sp >= eol) {
                // We can't split the header, it isn't valid.
                // This may be OK if this is a rename patch.
                //
                return eol;
            }
            final int bStart = nextLF(buf, sp, '/');
            if (bStart >= eol)
                return eol;

            // If buffer[aStart..sp - 1] = buffer[bStart..eol - 1]
            // we have a valid split.
            //
            if (eq(aStart, sp - 1, bStart, eol - 1)) {
                if (buf[bol] == '"') {
                    // We're a double quoted name. The region better end
                    // in a double quote too, and we need to decode the
                    // characters before reading the name.
                    //
                    if (buf[sp - 2] != '"') {
                        return eol;
                    }
                    oldPath = QuotedString.GIT_PATH.dequote(buf, bol, sp - 1);
                    oldPath = p1(oldPath);
                } else {
                    oldPath = decode(UTF_8, buf, aStart, sp - 1);
                }
                newPath = oldPath;
                return eol;
            }

            // This split wasn't correct. Move past the space and try
            // another split as the space must be part of the file name.
            //
            ptr = sp;
        }

        return eol;
    }

    int parseGitHeaders(int ptr, int end) {
        while (ptr < end) {
            final int eol = nextLF(buf, ptr);
            if (isHunkHdr(buf, ptr, eol) >= 1) {
                // First hunk header; break out and parse them later.
                break;

            } else if (match(buf, ptr, OLD_NAME) >= 0) {
                parseOldName(ptr, eol);

            } else if (match(buf, ptr, NEW_NAME) >= 0) {
                parseNewName(ptr, eol);

            } else if (match(buf, ptr, OLD_MODE) >= 0) {
                oldMode = parseFileMode(ptr + OLD_MODE.length, eol);

            } else if (match(buf, ptr, NEW_MODE) >= 0) {
                newMode = parseFileMode(ptr + NEW_MODE.length, eol);

            } else if (match(buf, ptr, DELETED_FILE_MODE) >= 0) {
                oldMode = parseFileMode(ptr + DELETED_FILE_MODE.length, eol);
                newMode = FileMode.MISSING;
                changeType = ChangeType.DELETE;

            } else if (match(buf, ptr, NEW_FILE_MODE) >= 0) {
                parseNewFileMode(ptr, eol);

            } else if (match(buf, ptr, COPY_FROM) >= 0) {
                oldPath = parseName(oldPath, ptr + COPY_FROM.length, eol);
                changeType = ChangeType.COPY;

            } else if (match(buf, ptr, COPY_TO) >= 0) {
                newPath = parseName(newPath, ptr + COPY_TO.length, eol);
                changeType = ChangeType.COPY;

            } else if (match(buf, ptr, RENAME_OLD) >= 0) {
                oldPath = parseName(oldPath, ptr + RENAME_OLD.length, eol);
                changeType = ChangeType.RENAME;

            } else if (match(buf, ptr, RENAME_NEW) >= 0) {
                newPath = parseName(newPath, ptr + RENAME_NEW.length, eol);
                changeType = ChangeType.RENAME;

            } else if (match(buf, ptr, RENAME_FROM) >= 0) {
                oldPath = parseName(oldPath, ptr + RENAME_FROM.length, eol);
                changeType = ChangeType.RENAME;

            } else if (match(buf, ptr, RENAME_TO) >= 0) {
                newPath = parseName(newPath, ptr + RENAME_TO.length, eol);
                changeType = ChangeType.RENAME;

            } else if (match(buf, ptr, SIMILARITY_INDEX) >= 0) {
                score = parseBase10(buf, ptr + SIMILARITY_INDEX.length, null);

            } else if (match(buf, ptr, DISSIMILARITY_INDEX) >= 0) {
                score = parseBase10(buf, ptr + DISSIMILARITY_INDEX.length, null);

            } else if (match(buf, ptr, INDEX) >= 0) {
                parseIndexLine(ptr + INDEX.length, eol);

            } else {
                // Probably an empty patch (stat dirty).
                break;
            }

            ptr = eol;
        }
        return ptr;
    }

    void parseOldName(int ptr, int eol) {
        oldPath = p1(parseName(oldPath, ptr + OLD_NAME.length, eol));
        if (oldPath == DEV_NULL)
            changeType = ChangeType.ADD;
    }

    void parseNewName(int ptr, int eol) {
        newPath = p1(parseName(newPath, ptr + NEW_NAME.length, eol));
        if (newPath == DEV_NULL)
            changeType = ChangeType.DELETE;
    }

    void parseNewFileMode(int ptr, int eol) {
        oldMode = FileMode.MISSING;
        newMode = parseFileMode(ptr + NEW_FILE_MODE.length, eol);
        changeType = ChangeType.ADD;
    }

    int parseTraditionalHeaders(int ptr, int end) {
        while (ptr < end) {
            final int eol = nextLF(buf, ptr);
            if (isHunkHdr(buf, ptr, eol) >= 1) {
                // First hunk header; break out and parse them later.
                break;

            } else if (match(buf, ptr, OLD_NAME) >= 0) {
                parseOldName(ptr, eol);

            } else if (match(buf, ptr, NEW_NAME) >= 0) {
                parseNewName(ptr, eol);

            } else {
                // Possibly an empty patch.
                break;
            }

            ptr = eol;
        }
        return ptr;
    }

    private String parseName(String expect, int ptr, int end) {
        if (ptr == end)
            return expect;

        String r;
        if (buf[ptr] == '"') {
            // New style GNU diff format
            //
            r = QuotedString.GIT_PATH.dequote(buf, ptr, end - 1);
        } else {
            // Older style GNU diff format, an optional tab ends the name.
            //
            int tab = end;
            while (ptr < tab && buf[tab - 1] != '\t')
                tab--;
            if (ptr == tab)
                tab = end;
            r = decode(UTF_8, buf, ptr, tab - 1);
        }

        if (r.equals(DEV_NULL))
            r = DEV_NULL;
        return r;
    }

    private static String p1(final String r) {
        final int s = r.indexOf('/');
        return s > 0 ? r.substring(s + 1) : r;
    }

    FileMode parseFileMode(int ptr, int end) {
        int tmp = 0;
        while (ptr < end - 1) {
            tmp <<= 3;
            tmp += buf[ptr++] - '0';
        }
        return FileMode.fromBits(tmp);
    }

    void parseIndexLine(int ptr, int end) {
        // "index $asha1..$bsha1[ $mode]" where $asha1 and $bsha1
        // can be unique abbreviations
        //
        final int dot2 = nextLF(buf, ptr, '.');
        final int mode = nextLF(buf, dot2, ' ');

        oldId = AbbreviatedObjectId.fromString(buf, ptr, dot2 - 1);
        newId = AbbreviatedObjectId.fromString(buf, dot2 + 1, mode - 1);

        if (mode < end)
            newMode = oldMode = parseFileMode(mode, end);
    }

    private boolean eq(int aPtr, int aEnd, int bPtr, int bEnd) {
        if (aEnd - aPtr != bEnd - bPtr) {
            return false;
        }
        while (aPtr < aEnd) {
            if (buf[aPtr++] != buf[bPtr++])
                return false;
        }
        return true;
    }

    /**
     * Determine if this is a patch hunk header.
     *
     * @param buf
     *            the buffer to scan
     * @param start
     *            first position in the buffer to evaluate
     * @param end
     *            last position to consider; usually the end of the buffer (
     *            <code>buf.length</code>) or the first position on the next
     *            line. This is only used to avoid very long runs of '@' from
     *            killing the scan loop.
     * @return the number of "ancestor revisions" in the hunk header. A
     *         traditional two-way diff ("@@ -...") returns 1; a combined diff
     *         for a 3 way-merge returns 3. If this is not a hunk header, 0 is
     *         returned instead.
     */
    static int isHunkHdr(byte[] buf, int start, int end) {
        int ptr = start;
        while (ptr < end && buf[ptr] == '@')
            ptr++;
        if (ptr - start < 2)
            return 0;
        if (ptr == end || buf[ptr++] != ' ')
            return 0;
        if (ptr == end || buf[ptr++] != '-')
            return 0;
        return (ptr - 3) - start;
    }
}