RebuildCommitGraph.java

  1. /*
  2.  * Copyright (C) 2009-2010, Google Inc. 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.pgm.debug;

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

  12. import java.io.BufferedReader;
  13. import java.io.File;
  14. import java.io.FileInputStream;
  15. import java.io.IOException;
  16. import java.io.InputStreamReader;
  17. import java.text.MessageFormat;
  18. import java.util.ArrayList;
  19. import java.util.Date;
  20. import java.util.HashMap;
  21. import java.util.List;
  22. import java.util.ListIterator;
  23. import java.util.Map;

  24. import org.eclipse.jgit.errors.MissingObjectException;
  25. import org.eclipse.jgit.errors.ObjectWritingException;
  26. import org.eclipse.jgit.internal.storage.file.LockFile;
  27. import org.eclipse.jgit.lib.CommitBuilder;
  28. import org.eclipse.jgit.lib.Constants;
  29. import org.eclipse.jgit.lib.ObjectId;
  30. import org.eclipse.jgit.lib.ObjectIdRef;
  31. import org.eclipse.jgit.lib.ObjectInserter;
  32. import org.eclipse.jgit.lib.PersonIdent;
  33. import org.eclipse.jgit.lib.ProgressMonitor;
  34. import org.eclipse.jgit.lib.Ref;
  35. import org.eclipse.jgit.lib.RefUpdate;
  36. import org.eclipse.jgit.lib.RefWriter;
  37. import org.eclipse.jgit.lib.TextProgressMonitor;
  38. import org.eclipse.jgit.pgm.Command;
  39. import org.eclipse.jgit.pgm.TextBuiltin;
  40. import org.eclipse.jgit.pgm.internal.CLIText;
  41. import org.eclipse.jgit.revwalk.RevWalk;
  42. import org.kohsuke.args4j.Argument;
  43. import org.kohsuke.args4j.Option;

  44. /**
  45.  * Recreates a repository from another one's commit graph.
  46.  * <p>
  47.  * <b>Do not run this on a repository unless you want to destroy it.</b>
  48.  * <p>
  49.  * To create the input files, in the source repository use:
  50.  *
  51.  * <pre>
  52.  * git for-each-ref &gt;in.refs
  53.  * git log --all '--pretty=format:%H %ct %P' &gt;in.dag
  54.  * </pre>
  55.  * <p>
  56.  * Run the rebuild in either an empty repository, or a clone of the source. Any
  57.  * missing commits (which might be the entire graph) will be created. All refs
  58.  * will be modified to match the input exactly, which means some refs may be
  59.  * deleted from the current repository.
  60.  * <p>
  61.  */
  62. @Command(usage = "usage_RebuildCommitGraph")
  63. class RebuildCommitGraph extends TextBuiltin {
  64.     private static final String REALLY = "--destroy-this-repository"; //$NON-NLS-1$

  65.     @Option(name = REALLY, usage = "usage_approveDestructionOfRepository")
  66.     boolean really;

  67.     @Argument(index = 0, required = true, metaVar = "metaVar_refs", usage = "usage_forEachRefOutput")
  68.     File refList;

  69.     @Argument(index = 1, required = true, metaVar = "metaVar_refs", usage = "usage_logAllPretty")
  70.     File graph;

  71.     private final ProgressMonitor pm = new TextProgressMonitor(errw);

  72.     private Map<ObjectId, ObjectId> rewrites = new HashMap<>();

  73.     /** {@inheritDoc} */
  74.     @Override
  75.     protected void run() throws Exception {
  76.         if (!really && db.getRefDatabase().hasRefs()) {
  77.             File directory = db.getDirectory();
  78.             String absolutePath = directory == null ? "null" //$NON-NLS-1$
  79.                     : directory.getAbsolutePath();
  80.             errw.println(
  81.                 MessageFormat.format(CLIText.get().fatalThisProgramWillDestroyTheRepository
  82.                     , absolutePath, REALLY));
  83.             throw die(CLIText.get().needApprovalToDestroyCurrentRepository);
  84.         }
  85.         if (!refList.isFile())
  86.             throw die(MessageFormat.format(CLIText.get().noSuchFile, refList.getPath()));
  87.         if (!graph.isFile())
  88.             throw die(MessageFormat.format(CLIText.get().noSuchFile, graph.getPath()));

  89.         recreateCommitGraph();
  90.         detachHead();
  91.         deleteAllRefs();
  92.         recreateRefs();
  93.     }

  94.     private void recreateCommitGraph() throws IOException {
  95.         final Map<ObjectId, ToRewrite> toRewrite = new HashMap<>();
  96.         List<ToRewrite> queue = new ArrayList<>();
  97.         try (RevWalk rw = new RevWalk(db);
  98.                 final BufferedReader br = new BufferedReader(
  99.                         new InputStreamReader(new FileInputStream(graph),
  100.                                 UTF_8))) {
  101.             String line;
  102.             while ((line = br.readLine()) != null) {
  103.                 final String[] parts = line.split("[ \t]{1,}"); //$NON-NLS-1$
  104.                 final ObjectId oldId = ObjectId.fromString(parts[0]);
  105.                 try {
  106.                     rw.parseCommit(oldId);
  107.                     // We have it already. Don't rewrite it.
  108.                     continue;
  109.                 } catch (MissingObjectException mue) {
  110.                     // Fall through and rewrite it.
  111.                 }

  112.                 final long time = Long.parseLong(parts[1]) * 1000L;
  113.                 final ObjectId[] parents = new ObjectId[parts.length - 2];
  114.                 for (int i = 0; i < parents.length; i++) {
  115.                     parents[i] = ObjectId.fromString(parts[2 + i]);
  116.                 }

  117.                 final ToRewrite t = new ToRewrite(oldId, time, parents);
  118.                 toRewrite.put(oldId, t);
  119.                 queue.add(t);
  120.             }
  121.         }

  122.         pm.beginTask("Rewriting commits", queue.size()); //$NON-NLS-1$
  123.         try (ObjectInserter oi = db.newObjectInserter()) {
  124.             final ObjectId emptyTree = oi.insert(Constants.OBJ_TREE,
  125.                     new byte[] {});
  126.             final PersonIdent me = new PersonIdent("jgit rebuild-commitgraph", //$NON-NLS-1$
  127.                     "rebuild-commitgraph@localhost"); //$NON-NLS-1$
  128.             while (!queue.isEmpty()) {
  129.                 final ListIterator<ToRewrite> itr = queue
  130.                         .listIterator(queue.size());
  131.                 queue = new ArrayList<>();
  132.                 REWRITE: while (itr.hasPrevious()) {
  133.                     final ToRewrite t = itr.previous();
  134.                     final ObjectId[] newParents = new ObjectId[t.oldParents.length];
  135.                     for (int k = 0; k < t.oldParents.length; k++) {
  136.                         final ToRewrite p = toRewrite.get(t.oldParents[k]);
  137.                         if (p != null) {
  138.                             if (p.newId == null) {
  139.                                 // Must defer until after the parent is
  140.                                 // rewritten.
  141.                                 queue.add(t);
  142.                                 continue REWRITE;
  143.                             }
  144.                             newParents[k] = p.newId;
  145.                         } else {
  146.                             // We have the old parent object. Use it.
  147.                             //
  148.                             newParents[k] = t.oldParents[k];
  149.                         }
  150.                     }

  151.                     final CommitBuilder newc = new CommitBuilder();
  152.                     newc.setTreeId(emptyTree);
  153.                     newc.setAuthor(new PersonIdent(me, new Date(t.commitTime)));
  154.                     newc.setCommitter(newc.getAuthor());
  155.                     newc.setParentIds(newParents);
  156.                     newc.setMessage("ORIGINAL " + t.oldId.name() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
  157.                     t.newId = oi.insert(newc);
  158.                     rewrites.put(t.oldId, t.newId);
  159.                     pm.update(1);
  160.                 }
  161.             }
  162.             oi.flush();
  163.         }
  164.         pm.endTask();
  165.     }

  166.     private static class ToRewrite {
  167.         final ObjectId oldId;

  168.         final long commitTime;

  169.         final ObjectId[] oldParents;

  170.         ObjectId newId;

  171.         ToRewrite(ObjectId o, long t, ObjectId[] p) {
  172.             oldId = o;
  173.             commitTime = t;
  174.             oldParents = p;
  175.         }
  176.     }

  177.     private void detachHead() throws IOException {
  178.         final String head = db.getFullBranch();
  179.         final ObjectId id = db.resolve(Constants.HEAD);
  180.         if (!ObjectId.isId(head) && id != null) {
  181.             final LockFile lf;
  182.             lf = new LockFile(new File(db.getDirectory(), Constants.HEAD));
  183.             if (!lf.lock())
  184.                 throw new IOException(MessageFormat.format(CLIText.get().cannotLock, Constants.HEAD));
  185.             lf.write(id);
  186.             if (!lf.commit())
  187.                 throw new IOException(CLIText.get().cannotDeatchHEAD);
  188.         }
  189.     }

  190.     private void deleteAllRefs() throws Exception {
  191.         final RevWalk rw = new RevWalk(db);
  192.         for (Ref r : db.getRefDatabase().getRefs()) {
  193.             if (Constants.HEAD.equals(r.getName()))
  194.                 continue;
  195.             final RefUpdate u = db.updateRef(r.getName());
  196.             u.setForceUpdate(true);
  197.             u.delete(rw);
  198.         }
  199.     }

  200.     private void recreateRefs() throws Exception {
  201.         final Map<String, Ref> refs = computeNewRefs();
  202.         new RefWriter(refs.values()) {
  203.             @Override
  204.             protected void writeFile(String name, byte[] content)
  205.                     throws IOException {
  206.                 final File file = new File(db.getDirectory(), name);
  207.                 final LockFile lck = new LockFile(file);
  208.                 if (!lck.lock())
  209.                     throw new ObjectWritingException(MessageFormat.format(CLIText.get().cantWrite, file));
  210.                 try {
  211.                     lck.write(content);
  212.                 } catch (IOException ioe) {
  213.                     throw new ObjectWritingException(
  214.                             MessageFormat.format(CLIText.get().cantWrite, file),
  215.                             ioe);
  216.                 }
  217.                 if (!lck.commit())
  218.                     throw new ObjectWritingException(MessageFormat.format(CLIText.get().cantWrite, file));
  219.             }
  220.         }.writePackedRefs();
  221.     }

  222.     private Map<String, Ref> computeNewRefs() throws IOException {
  223.         final Map<String, Ref> refs = new HashMap<>();
  224.         try (RevWalk rw = new RevWalk(db);
  225.                 BufferedReader br = new BufferedReader(
  226.                         new InputStreamReader(new FileInputStream(refList),
  227.                                 UTF_8))) {
  228.             String line;
  229.             while ((line = br.readLine()) != null) {
  230.                 final String[] parts = line.split("[ \t]{1,}"); //$NON-NLS-1$
  231.                 final ObjectId origId = ObjectId.fromString(parts[0]);
  232.                 final String type = parts[1];
  233.                 final String name = parts[2];

  234.                 ObjectId id = rewrites.get(origId);
  235.                 if (id == null)
  236.                     id = origId;
  237.                 try {
  238.                     rw.parseAny(id);
  239.                 } catch (MissingObjectException mue) {
  240.                     if (!Constants.TYPE_COMMIT.equals(type)) {
  241.                         errw.println(MessageFormat.format(CLIText.get().skippingObject, type, name));
  242.                         continue;
  243.                     }
  244.                     MissingObjectException mue1 = new MissingObjectException(id, type);
  245.                     mue1.initCause(mue);
  246.                     throw mue1;
  247.                 }
  248.                 refs.put(name, new ObjectIdRef.Unpeeled(Ref.Storage.PACKED,
  249.                         name, id));
  250.             }
  251.         }
  252.         return refs;
  253.     }
  254. }