RebaseTodoFile.java

  1. /*
  2.  * Copyright (C) 2013, Christian Halstrick <christian.halstrick@sap.com> and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.lib;

  11. import static java.nio.charset.StandardCharsets.UTF_8;

  12. import java.io.BufferedOutputStream;
  13. import java.io.File;
  14. import java.io.FileOutputStream;
  15. import java.io.IOException;
  16. import java.io.OutputStream;
  17. import java.util.LinkedList;
  18. import java.util.List;

  19. import org.eclipse.jgit.lib.RebaseTodoLine.Action;
  20. import org.eclipse.jgit.util.IO;
  21. import org.eclipse.jgit.util.RawParseUtils;

  22. /**
  23.  * Offers methods to read and write files formatted like the git-rebase-todo
  24.  * file
  25.  *
  26.  * @since 3.2
  27.  */
  28. public class RebaseTodoFile {
  29.     private Repository repo;

  30.     /**
  31.      * Constructor for RebaseTodoFile.
  32.      *
  33.      * @param repo
  34.      *            a {@link org.eclipse.jgit.lib.Repository} object.
  35.      */
  36.     public RebaseTodoFile(Repository repo) {
  37.         this.repo = repo;
  38.     }

  39.     /**
  40.      * Read a file formatted like the git-rebase-todo file. The "done" file is
  41.      * also formatted like the git-rebase-todo file. These files can be found in
  42.      * .git/rebase-merge/ or .git/rebase-append/ folders.
  43.      *
  44.      * @param path
  45.      *            path to the file relative to the repository's git-dir. E.g.
  46.      *            "rebase-merge/git-rebase-todo" or "rebase-append/done"
  47.      * @param includeComments
  48.      *            <code>true</code> if also comments should be reported
  49.      * @return the list of steps
  50.      * @throws java.io.IOException
  51.      */
  52.     public List<RebaseTodoLine> readRebaseTodo(String path,
  53.             boolean includeComments) throws IOException {
  54.         byte[] buf = IO.readFully(new File(repo.getDirectory(), path));
  55.         int ptr = 0;
  56.         int tokenBegin = 0;
  57.         List<RebaseTodoLine> r = new LinkedList<>();
  58.         while (ptr < buf.length) {
  59.             tokenBegin = ptr;
  60.             ptr = RawParseUtils.nextLF(buf, ptr);
  61.             int lineStart = tokenBegin;
  62.             int lineEnd = ptr - 2;
  63.             if (lineEnd >= 0 && buf[lineEnd] == '\r')
  64.                 lineEnd--;
  65.             // Handle comments
  66.             if (buf[tokenBegin] == '#') {
  67.                 if (includeComments)
  68.                     parseComments(buf, tokenBegin, r, lineEnd);
  69.             } else {
  70.                 // skip leading spaces+tabs+cr
  71.                 tokenBegin = nextParsableToken(buf, tokenBegin, lineEnd);
  72.                 // Handle empty lines (maybe empty after skipping leading
  73.                 // whitespace)
  74.                 if (tokenBegin == -1) {
  75.                     if (includeComments)
  76.                         r.add(new RebaseTodoLine(RawParseUtils.decode(buf,
  77.                                 lineStart, 1 + lineEnd)));
  78.                     continue;
  79.                 }
  80.                 RebaseTodoLine line = parseLine(buf, tokenBegin, lineEnd);
  81.                 if (line == null)
  82.                     continue;
  83.                 r.add(line);
  84.             }
  85.         }
  86.         return r;
  87.     }

  88.     private static void parseComments(byte[] buf, int tokenBegin,
  89.             List<RebaseTodoLine> r, int lineEnd) {
  90.         RebaseTodoLine line = null;
  91.         String commentString = RawParseUtils.decode(buf,
  92.                 tokenBegin, lineEnd + 1);
  93.         try {
  94.             int skip = tokenBegin + 1; // skip '#'
  95.             skip = nextParsableToken(buf, skip, lineEnd);
  96.             if (skip != -1) {
  97.                 // try to parse the line as non-comment
  98.                 line = parseLine(buf, skip, lineEnd);
  99.                 if (line != null) {
  100.                     // successfully parsed as non-comment line
  101.                     // mark this line as a comment explicitly
  102.                     line.setAction(Action.COMMENT);
  103.                     // use the read line as comment string
  104.                     line.setComment(commentString);
  105.                 }
  106.             }
  107.         } catch (Exception e) {
  108.             // parsing as non-comment line failed
  109.             line = null;
  110.         } finally {
  111.             if (line == null)
  112.                 line = new RebaseTodoLine(commentString);
  113.             r.add(line);
  114.         }
  115.     }

  116.     /**
  117.      * Skip leading space, tab, CR and LF characters
  118.      *
  119.      * @param buf
  120.      * @param tokenBegin
  121.      * @param lineEnd
  122.      * @return the token within the range of the given {@code buf} that doesn't
  123.      *         need to be skipped, {@code -1} if no such token found within the
  124.      *         range (i.e. empty line)
  125.      */
  126.     private static int nextParsableToken(byte[] buf, int tokenBegin, int lineEnd) {
  127.         while (tokenBegin <= lineEnd
  128.                 && (buf[tokenBegin] == ' ' || buf[tokenBegin] == '\t' || buf[tokenBegin] == '\r'))
  129.             tokenBegin++;
  130.         if (tokenBegin > lineEnd)
  131.             return -1;
  132.         return tokenBegin;
  133.     }

  134.     private static RebaseTodoLine parseLine(byte[] buf, int tokenBegin,
  135.             int lineEnd) {
  136.         RebaseTodoLine.Action action = null;
  137.         AbbreviatedObjectId commit = null;

  138.         int nextSpace = RawParseUtils.next(buf, tokenBegin, ' ');
  139.         int tokenCount = 0;
  140.         while (tokenCount < 3 && nextSpace <= lineEnd) {
  141.             switch (tokenCount) {
  142.             case 0:
  143.                 String actionToken = new String(buf, tokenBegin,
  144.                         nextSpace - tokenBegin - 1, UTF_8);
  145.                 tokenBegin = nextSpace;
  146.                 action = RebaseTodoLine.Action.parse(actionToken);
  147.                 if (action == null)
  148.                     return null; // parsing failed
  149.                 break;
  150.             case 1:
  151.                 nextSpace = RawParseUtils.next(buf, tokenBegin, ' ');
  152.                 String commitToken;
  153.                 if (nextSpace > lineEnd + 1) {
  154.                     commitToken = new String(buf, tokenBegin,
  155.                             lineEnd - tokenBegin + 1, UTF_8);
  156.                 } else {
  157.                     commitToken = new String(buf, tokenBegin,
  158.                             nextSpace - tokenBegin - 1, UTF_8);
  159.                 }
  160.                 tokenBegin = nextSpace;
  161.                 commit = AbbreviatedObjectId.fromString(commitToken);
  162.                 break;
  163.             case 2:
  164.                 return new RebaseTodoLine(action, commit,
  165.                         RawParseUtils.decode(buf, tokenBegin, 1 + lineEnd));
  166.             }
  167.             tokenCount++;
  168.         }
  169.         if (tokenCount == 2)
  170.             return new RebaseTodoLine(action, commit, ""); //$NON-NLS-1$
  171.         return null;
  172.     }

  173.     /**
  174.      * Write a file formatted like a git-rebase-todo file.
  175.      *
  176.      * @param path
  177.      *            path to the file relative to the repository's git-dir. E.g.
  178.      *            "rebase-merge/git-rebase-todo" or "rebase-append/done"
  179.      * @param steps
  180.      *            the steps to be written
  181.      * @param append
  182.      *            whether to append to an existing file or to write a new file
  183.      * @throws java.io.IOException
  184.      */
  185.     public void writeRebaseTodoFile(String path, List<RebaseTodoLine> steps,
  186.             boolean append) throws IOException {
  187.         try (OutputStream fw = new BufferedOutputStream(new FileOutputStream(
  188.                 new File(repo.getDirectory(), path), append))) {
  189.             StringBuilder sb = new StringBuilder();
  190.             for (RebaseTodoLine step : steps) {
  191.                 sb.setLength(0);
  192.                 if (RebaseTodoLine.Action.COMMENT.equals(step.action))
  193.                     sb.append(step.getComment());
  194.                 else {
  195.                     sb.append(step.getAction().toToken());
  196.                     sb.append(" "); //$NON-NLS-1$
  197.                     sb.append(step.getCommit().name());
  198.                     sb.append(" "); //$NON-NLS-1$
  199.                     sb.append(step.getShortMessage().trim());
  200.                 }
  201.                 sb.append('\n');
  202.                 fw.write(Constants.encode(sb.toString()));
  203.             }
  204.         }
  205.     }
  206. }