View Javadoc
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  
11  package org.eclipse.jgit.pgm.debug;
12  
13  import static java.nio.charset.StandardCharsets.UTF_8;
14  
15  import java.io.BufferedReader;
16  import java.io.File;
17  import java.io.FileInputStream;
18  import java.io.IOException;
19  import java.io.InputStreamReader;
20  import java.text.MessageFormat;
21  import java.util.ArrayList;
22  import java.util.Date;
23  import java.util.HashMap;
24  import java.util.List;
25  import java.util.ListIterator;
26  import java.util.Map;
27  
28  import org.eclipse.jgit.errors.MissingObjectException;
29  import org.eclipse.jgit.errors.ObjectWritingException;
30  import org.eclipse.jgit.internal.storage.file.LockFile;
31  import org.eclipse.jgit.lib.CommitBuilder;
32  import org.eclipse.jgit.lib.Constants;
33  import org.eclipse.jgit.lib.ObjectId;
34  import org.eclipse.jgit.lib.ObjectIdRef;
35  import org.eclipse.jgit.lib.ObjectInserter;
36  import org.eclipse.jgit.lib.PersonIdent;
37  import org.eclipse.jgit.lib.ProgressMonitor;
38  import org.eclipse.jgit.lib.Ref;
39  import org.eclipse.jgit.lib.RefUpdate;
40  import org.eclipse.jgit.lib.RefWriter;
41  import org.eclipse.jgit.lib.TextProgressMonitor;
42  import org.eclipse.jgit.pgm.Command;
43  import org.eclipse.jgit.pgm.TextBuiltin;
44  import org.eclipse.jgit.pgm.internal.CLIText;
45  import org.eclipse.jgit.revwalk.RevWalk;
46  import org.kohsuke.args4j.Argument;
47  import org.kohsuke.args4j.Option;
48  
49  /**
50   * Recreates a repository from another one's commit graph.
51   * <p>
52   * <b>Do not run this on a repository unless you want to destroy it.</b>
53   * <p>
54   * To create the input files, in the source repository use:
55   *
56   * <pre>
57   * git for-each-ref &gt;in.refs
58   * git log --all '--pretty=format:%H %ct %P' &gt;in.dag
59   * </pre>
60   * <p>
61   * Run the rebuild in either an empty repository, or a clone of the source. Any
62   * missing commits (which might be the entire graph) will be created. All refs
63   * will be modified to match the input exactly, which means some refs may be
64   * deleted from the current repository.
65   * <p>
66   */
67  @Command(usage = "usage_RebuildCommitGraph")
68  class RebuildCommitGraph extends TextBuiltin {
69  	private static final String REALLY = "--destroy-this-repository"; //$NON-NLS-1$
70  
71  	@Option(name = REALLY, usage = "usage_approveDestructionOfRepository")
72  	boolean really;
73  
74  	@Argument(index = 0, required = true, metaVar = "metaVar_refs", usage = "usage_forEachRefOutput")
75  	File refList;
76  
77  	@Argument(index = 1, required = true, metaVar = "metaVar_refs", usage = "usage_logAllPretty")
78  	File graph;
79  
80  	private final ProgressMonitor pm = new TextProgressMonitor(errw);
81  
82  	private Map<ObjectId, ObjectId> rewrites = new HashMap<>();
83  
84  	/** {@inheritDoc} */
85  	@Override
86  	protected void run() throws Exception {
87  		if (!really && db.getRefDatabase().hasRefs()) {
88  			File directory = db.getDirectory();
89  			String absolutePath = directory == null ? "null" //$NON-NLS-1$
90  					: directory.getAbsolutePath();
91  			errw.println(
92  				MessageFormat.format(CLIText.get().fatalThisProgramWillDestroyTheRepository
93  					, absolutePath, REALLY));
94  			throw die(CLIText.get().needApprovalToDestroyCurrentRepository);
95  		}
96  		if (!refList.isFile())
97  			throw die(MessageFormat.format(CLIText.get().noSuchFile, refList.getPath()));
98  		if (!graph.isFile())
99  			throw die(MessageFormat.format(CLIText.get().noSuchFile, graph.getPath()));
100 
101 		recreateCommitGraph();
102 		detachHead();
103 		deleteAllRefs();
104 		recreateRefs();
105 	}
106 
107 	private void recreateCommitGraph() throws IOException {
108 		final Map<ObjectId, ToRewrite> toRewrite = new HashMap<>();
109 		List<ToRewrite> queue = new ArrayList<>();
110 		try (RevWalklk/RevWalk.html#RevWalk">RevWalk rw = new RevWalk(db);
111 				final BufferedReader br = new BufferedReader(
112 						new InputStreamReader(new FileInputStream(graph),
113 								UTF_8))) {
114 			String line;
115 			while ((line = br.readLine()) != null) {
116 				final String[] parts = line.split("[ \t]{1,}"); //$NON-NLS-1$
117 				final ObjectId oldId = ObjectId.fromString(parts[0]);
118 				try {
119 					rw.parseCommit(oldId);
120 					// We have it already. Don't rewrite it.
121 					continue;
122 				} catch (MissingObjectException mue) {
123 					// Fall through and rewrite it.
124 				}
125 
126 				final long time = Long.parseLong(parts[1]) * 1000L;
127 				final ObjectIdhtml#ObjectId">ObjectId[] parents = new ObjectId[parts.length - 2];
128 				for (int i = 0; i < parents.length; i++) {
129 					parents[i] = ObjectId.fromString(parts[2 + i]);
130 				}
131 
132 				final ToRewrite t = new ToRewrite(oldId, time, parents);
133 				toRewrite.put(oldId, t);
134 				queue.add(t);
135 			}
136 		}
137 
138 		pm.beginTask("Rewriting commits", queue.size()); //$NON-NLS-1$
139 		try (ObjectInserter oi = db.newObjectInserter()) {
140 			final ObjectId emptyTree = oi.insert(Constants.OBJ_TREE,
141 					new byte[] {});
142 			final PersonIdentnIdent.html#PersonIdent">PersonIdent me = new PersonIdent("jgit rebuild-commitgraph", //$NON-NLS-1$
143 					"rebuild-commitgraph@localhost"); //$NON-NLS-1$
144 			while (!queue.isEmpty()) {
145 				final ListIterator<ToRewrite> itr = queue
146 						.listIterator(queue.size());
147 				queue = new ArrayList<>();
148 				REWRITE: while (itr.hasPrevious()) {
149 					final ToRewrite t = itr.previous();
150 					final ObjectIdl#ObjectId">ObjectId[] newParents = new ObjectId[t.oldParents.length];
151 					for (int k = 0; k < t.oldParents.length; k++) {
152 						final ToRewrite p = toRewrite.get(t.oldParents[k]);
153 						if (p != null) {
154 							if (p.newId == null) {
155 								// Must defer until after the parent is
156 								// rewritten.
157 								queue.add(t);
158 								continue REWRITE;
159 							}
160 							newParents[k] = p.newId;
161 						} else {
162 							// We have the old parent object. Use it.
163 							//
164 							newParents[k] = t.oldParents[k];
165 						}
166 					}
167 
168 					final CommitBuilderlder.html#CommitBuilder">CommitBuilder newc = new CommitBuilder();
169 					newc.setTreeId(emptyTree);
170 					newc.setAuthor(new PersonIdent(me, new Date(t.commitTime)));
171 					newc.setCommitter(newc.getAuthor());
172 					newc.setParentIds(newParents);
173 					newc.setMessage("ORIGINAL " + t.oldId.name() + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
174 					t.newId = oi.insert(newc);
175 					rewrites.put(t.oldId, t.newId);
176 					pm.update(1);
177 				}
178 			}
179 			oi.flush();
180 		}
181 		pm.endTask();
182 	}
183 
184 	private static class ToRewrite {
185 		final ObjectId oldId;
186 
187 		final long commitTime;
188 
189 		final ObjectId[] oldParents;
190 
191 		ObjectId newId;
192 
193 		ToRewrite(ObjectIdctId.html#ObjectId">ObjectId o, long t, ObjectId[] p) {
194 			oldId = o;
195 			commitTime = t;
196 			oldParents = p;
197 		}
198 	}
199 
200 	private void detachHead() throws IOException {
201 		final String head = db.getFullBranch();
202 		final ObjectId id = db.resolve(Constants.HEAD);
203 		if (!ObjectId.isId(head) && id != null) {
204 			final LockFile lf;
205 			lf = new LockFile(new File(db.getDirectory(), Constants.HEAD));
206 			if (!lf.lock())
207 				throw new IOException(MessageFormat.format(CLIText.get().cannotLock, Constants.HEAD));
208 			lf.write(id);
209 			if (!lf.commit())
210 				throw new IOException(CLIText.get().cannotDeatchHEAD);
211 		}
212 	}
213 
214 	private void deleteAllRefs() throws Exception {
215 		final RevWalklk/RevWalk.html#RevWalk">RevWalk rw = new RevWalk(db);
216 		for (Ref r : db.getRefDatabase().getRefs()) {
217 			if (Constants.HEAD.equals(r.getName()))
218 				continue;
219 			final RefUpdate u = db.updateRef(r.getName());
220 			u.setForceUpdate(true);
221 			u.delete(rw);
222 		}
223 	}
224 
225 	private void recreateRefs() throws Exception {
226 		final Map<String, Ref> refs = computeNewRefs();
227 		new RefWriter(refs.values()) {
228 			@Override
229 			protected void writeFile(String name, byte[] content)
230 					throws IOException {
231 				final File file = new File(db.getDirectory(), name);
232 				final LockFilel/storage/file/LockFile.html#LockFile">LockFile lck = new LockFile(file);
233 				if (!lck.lock())
234 					throw new ObjectWritingException(MessageFormat.format(CLIText.get().cantWrite, file));
235 				try {
236 					lck.write(content);
237 				} catch (IOException ioe) {
238 					throw new ObjectWritingException(
239 							MessageFormat.format(CLIText.get().cantWrite, file),
240 							ioe);
241 				}
242 				if (!lck.commit())
243 					throw new ObjectWritingException(MessageFormat.format(CLIText.get().cantWrite, file));
244 			}
245 		}.writePackedRefs();
246 	}
247 
248 	private Map<String, Ref> computeNewRefs() throws IOException {
249 		final Map<String, Ref> refs = new HashMap<>();
250 		try (RevWalklk/RevWalk.html#RevWalk">RevWalk rw = new RevWalk(db);
251 				BufferedReader br = new BufferedReader(
252 						new InputStreamReader(new FileInputStream(refList),
253 								UTF_8))) {
254 			String line;
255 			while ((line = br.readLine()) != null) {
256 				final String[] parts = line.split("[ \t]{1,}"); //$NON-NLS-1$
257 				final ObjectId origId = ObjectId.fromString(parts[0]);
258 				final String type = parts[1];
259 				final String name = parts[2];
260 
261 				ObjectId id = rewrites.get(origId);
262 				if (id == null)
263 					id = origId;
264 				try {
265 					rw.parseAny(id);
266 				} catch (MissingObjectException mue) {
267 					if (!Constants.TYPE_COMMIT.equals(type)) {
268 						errw.println(MessageFormat.format(CLIText.get().skippingObject, type, name));
269 						continue;
270 					}
271 					MissingObjectException mue1 = new MissingObjectException(id, type);
272 					mue1.initCause(mue);
273 					throw mue1;
274 				}
275 				refs.put(name, new ObjectIdRef.Unpeeled(Ref.Storage.PACKED,
276 						name, id));
277 			}
278 		}
279 		return refs;
280 	}
281 }