/* * Copyright (C) 2013, Christian Halstrick and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.lib; import static java.nio.charset.StandardCharsets.UTF_8; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.LinkedList; import java.util.List; import org.eclipse.jgit.lib.RebaseTodoLine.Action; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; /** * Offers methods to read and write files formatted like the git-rebase-todo * file * * @since 3.2 */ public class RebaseTodoFile { private Repository repo; /** * Constructor for RebaseTodoFile. * * @param repo * a {@link org.eclipse.jgit.lib.Repository} object. */ public RebaseTodoFile(Repository repo) { this.repo = repo; } /** * Read a file formatted like the git-rebase-todo file. The "done" file is * also formatted like the git-rebase-todo file. These files can be found in * .git/rebase-merge/ or .git/rebase-append/ folders. * * @param path * path to the file relative to the repository's git-dir. E.g. * "rebase-merge/git-rebase-todo" or "rebase-append/done" * @param includeComments * true if also comments should be reported * @return the list of steps * @throws java.io.IOException */ public List readRebaseTodo(String path, boolean includeComments) throws IOException { byte[] buf = IO.readFully(new File(repo.getDirectory(), path)); int ptr = 0; int tokenBegin = 0; List r = new LinkedList<>(); while (ptr < buf.length) { tokenBegin = ptr; ptr = RawParseUtils.nextLF(buf, ptr); int lineStart = tokenBegin; int lineEnd = ptr - 2; if (lineEnd >= 0 && buf[lineEnd] == '\r') lineEnd--; // Handle comments if (buf[tokenBegin] == '#') { if (includeComments) parseComments(buf, tokenBegin, r, lineEnd); } else { // skip leading spaces+tabs+cr tokenBegin = nextParsableToken(buf, tokenBegin, lineEnd); // Handle empty lines (maybe empty after skipping leading // whitespace) if (tokenBegin == -1) { if (includeComments) r.add(new RebaseTodoLine(RawParseUtils.decode(buf, lineStart, 1 + lineEnd))); continue; } RebaseTodoLine line = parseLine(buf, tokenBegin, lineEnd); if (line == null) continue; r.add(line); } } return r; } private static void parseComments(byte[] buf, int tokenBegin, List r, int lineEnd) { RebaseTodoLine line = null; String commentString = RawParseUtils.decode(buf, tokenBegin, lineEnd + 1); try { int skip = tokenBegin + 1; // skip '#' skip = nextParsableToken(buf, skip, lineEnd); if (skip != -1) { // try to parse the line as non-comment line = parseLine(buf, skip, lineEnd); if (line != null) { // successfully parsed as non-comment line // mark this line as a comment explicitly line.setAction(Action.COMMENT); // use the read line as comment string line.setComment(commentString); } } } catch (Exception e) { // parsing as non-comment line failed line = null; } finally { if (line == null) line = new RebaseTodoLine(commentString); r.add(line); } } /** * Skip leading space, tab, CR and LF characters * * @param buf * @param tokenBegin * @param lineEnd * @return the token within the range of the given {@code buf} that doesn't * need to be skipped, {@code -1} if no such token found within the * range (i.e. empty line) */ private static int nextParsableToken(byte[] buf, int tokenBegin, int lineEnd) { while (tokenBegin <= lineEnd && (buf[tokenBegin] == ' ' || buf[tokenBegin] == '\t' || buf[tokenBegin] == '\r')) tokenBegin++; if (tokenBegin > lineEnd) return -1; return tokenBegin; } private static RebaseTodoLine parseLine(byte[] buf, int tokenBegin, int lineEnd) { RebaseTodoLine.Action action = null; AbbreviatedObjectId commit = null; int nextSpace = RawParseUtils.next(buf, tokenBegin, ' '); int tokenCount = 0; while (tokenCount < 3 && nextSpace <= lineEnd) { switch (tokenCount) { case 0: String actionToken = new String(buf, tokenBegin, nextSpace - tokenBegin - 1, UTF_8); tokenBegin = nextSpace; action = RebaseTodoLine.Action.parse(actionToken); if (action == null) return null; // parsing failed break; case 1: nextSpace = RawParseUtils.next(buf, tokenBegin, ' '); String commitToken; if (nextSpace > lineEnd + 1) { commitToken = new String(buf, tokenBegin, lineEnd - tokenBegin + 1, UTF_8); } else { commitToken = new String(buf, tokenBegin, nextSpace - tokenBegin - 1, UTF_8); } tokenBegin = nextSpace; commit = AbbreviatedObjectId.fromString(commitToken); break; case 2: return new RebaseTodoLine(action, commit, RawParseUtils.decode(buf, tokenBegin, 1 + lineEnd)); } tokenCount++; } if (tokenCount == 2) return new RebaseTodoLine(action, commit, ""); //$NON-NLS-1$ return null; } /** * Write a file formatted like a git-rebase-todo file. * * @param path * path to the file relative to the repository's git-dir. E.g. * "rebase-merge/git-rebase-todo" or "rebase-append/done" * @param steps * the steps to be written * @param append * whether to append to an existing file or to write a new file * @throws java.io.IOException */ public void writeRebaseTodoFile(String path, List steps, boolean append) throws IOException { try (OutputStream fw = new BufferedOutputStream(new FileOutputStream( new File(repo.getDirectory(), path), append))) { StringBuilder sb = new StringBuilder(); for (RebaseTodoLine step : steps) { sb.setLength(0); if (RebaseTodoLine.Action.COMMENT.equals(step.action)) sb.append(step.getComment()); else { sb.append(step.getAction().toToken()); sb.append(" "); //$NON-NLS-1$ sb.append(step.getCommit().name()); sb.append(" "); //$NON-NLS-1$ sb.append(step.getShortMessage().trim()); } sb.append('\n'); fw.write(Constants.encode(sb.toString())); } } } }