Mercurial > hg4j
comparison cmdline/org/tmatesoft/hg/console/Clone.java @ 186:44a34baabea0
Clone refactored into a command. HgBundle needs means to control its lifecycle, to be deleted when no longer needed
author | Artem Tikhomirov <tikhomirov.artem@gmail.com> |
---|---|
date | Thu, 14 Apr 2011 00:47:04 +0200 |
parents | 2c3e96674e2a |
children | 45dc79e545f5 |
comparison
equal
deleted
inserted
replaced
185:c6fa4dbfc458 | 186:44a34baabea0 |
---|---|
14 * the terms of a license other than GNU General Public License | 14 * the terms of a license other than GNU General Public License |
15 * contact TMate Software at support@hg4j.com | 15 * contact TMate Software at support@hg4j.com |
16 */ | 16 */ |
17 package org.tmatesoft.hg.console; | 17 package org.tmatesoft.hg.console; |
18 | 18 |
19 import static org.tmatesoft.hg.core.Nodeid.NULL; | 19 import java.io.File; |
20 import static org.tmatesoft.hg.internal.RequiresFile.*; | 20 import java.util.List; |
21 import static org.tmatesoft.hg.internal.RequiresFile.DOTENCODE; | |
22 import static org.tmatesoft.hg.internal.RequiresFile.FNCACHE; | |
23 | 21 |
24 import java.io.ByteArrayOutputStream; | 22 import org.tmatesoft.hg.core.HgCloneCommand; |
25 import java.io.File; | |
26 import java.io.FileOutputStream; | |
27 import java.io.IOException; | |
28 import java.net.URL; | |
29 import java.nio.ByteBuffer; | |
30 import java.util.ArrayList; | |
31 import java.util.Collections; | |
32 import java.util.LinkedList; | |
33 import java.util.List; | |
34 import java.util.TreeMap; | |
35 import java.util.zip.DeflaterOutputStream; | |
36 | |
37 import org.tmatesoft.hg.core.HgBadStateException; | |
38 import org.tmatesoft.hg.core.HgRepoFacade; | |
39 import org.tmatesoft.hg.core.Nodeid; | |
40 import org.tmatesoft.hg.internal.ByteArrayDataAccess; | |
41 import org.tmatesoft.hg.internal.DataAccess; | |
42 import org.tmatesoft.hg.internal.DigestHelper; | |
43 import org.tmatesoft.hg.internal.Internals; | |
44 import org.tmatesoft.hg.internal.RequiresFile; | |
45 import org.tmatesoft.hg.internal.RevlogStream; | |
46 import org.tmatesoft.hg.repo.HgBundle; | |
47 import org.tmatesoft.hg.repo.HgLookup; | 23 import org.tmatesoft.hg.repo.HgLookup; |
48 import org.tmatesoft.hg.repo.HgRemoteRepository; | 24 import org.tmatesoft.hg.repo.HgRemoteRepository; |
49 import org.tmatesoft.hg.repo.HgBundle.GroupElement; | |
50 import org.tmatesoft.hg.repo.HgRepository; | |
51 import org.tmatesoft.hg.util.PathRewrite; | |
52 | 25 |
53 /** | 26 /** |
54 * WORK IN PROGRESS, DO NOT USE | 27 * Initial clone of a repository. Creates a brand new repository and populates it from specified source. |
55 * | 28 * |
56 * @author Artem Tikhomirov | 29 * @author Artem Tikhomirov |
57 * @author TMate Software Ltd. | 30 * @author TMate Software Ltd. |
58 */ | 31 */ |
59 public class Clone { | 32 public class Clone { |
60 /* | 33 |
61 * Changegroup: | 34 // ran with args: svnkit c:\temp\hg\test-clone |
62 * http://mercurial.selenic.com/wiki/Merge | |
63 * http://mercurial.selenic.com/wiki/WireProtocol | |
64 * | |
65 * according to latter, bundleformat data is sent through zlib | |
66 * (there's no header like HG10?? with the server output, though, | |
67 * as one may expect according to http://mercurial.selenic.com/wiki/BundleFormat) | |
68 */ | |
69 public static void main(String[] args) throws Exception { | 35 public static void main(String[] args) throws Exception { |
70 Options cmdLineOpts = Options.parse(args); | 36 Options cmdLineOpts = Options.parse(args); |
71 HgRepoFacade hgRepo = new HgRepoFacade(); | 37 List<String> noOptsArgs = cmdLineOpts.getList(""); |
72 if (!hgRepo.init(cmdLineOpts.findRepository())) { | 38 if (noOptsArgs.isEmpty()) { |
73 System.err.printf("Can't find repository in: %s\n", hgRepo.getRepository().getLocation()); | 39 System.err.println("Need at least one argument pointing to remote server to pull changes from"); |
74 return; | 40 return; |
75 } | 41 } |
76 File destDir = new File("/temp/hg/clone-01/"); | 42 HgCloneCommand cmd = new HgCloneCommand(); |
77 if (destDir.exists()) { | 43 String remoteRepo = noOptsArgs.get(0); |
78 if (!destDir.isDirectory()) { | 44 HgRemoteRepository hgRemote = new HgLookup().detectRemote(remoteRepo, null); |
79 throw new IllegalArgumentException(); | 45 if (hgRemote.isInvalid()) { |
80 } else if (destDir.list().length > 0) { | 46 System.err.printf("Remote repository %s is not valid", hgRemote.getLocation()); |
81 throw new IllegalArgumentException(); | 47 return; |
82 } | 48 } |
49 cmd.source(hgRemote); | |
50 if (noOptsArgs.size() > 1) { | |
51 cmd.destination(new File(noOptsArgs.get(1))); | |
83 } else { | 52 } else { |
84 destDir.mkdirs(); | 53 cmd.destination(new File(System.getProperty("user.dir"))); |
85 } | 54 } |
86 // if cloning remote repo, which can stream and no revision is specified - | 55 cmd.execute(); |
87 // can use 'stream_out' wireproto | |
88 // | |
89 // //////// 1. from Remote.java take code that asks changegroup from remote server and write it down to temp file | |
90 // //////// 2. then, read the file with HgBundle | |
91 // //////// 3. process changelog, memorize nodeids to index | |
92 // //////// 4. process manifest, using map from step 3, collect manifest nodeids | |
93 // //////// 5. process every file, using map from 3, and consult set from step 4 to ensure repo is correct | |
94 // access source | |
95 HgRemoteRepository remoteRepo = new HgLookup().detect(new URL("https://asd/hg/")); | |
96 // discover changes | |
97 HgBundle completeChanges = remoteRepo.getChanges(Collections.singletonList(NULL)); | |
98 WriteDownMate mate = new WriteDownMate(destDir); | |
99 // instantiate new repo in the destdir | |
100 mate.initEmptyRepository(); | |
101 // pull changes | |
102 completeChanges.inspectAll(mate); | |
103 mate.complete(); | |
104 // completeChanges.unlink(); | |
105 } | |
106 | |
107 private static class WriteDownMate implements HgBundle.Inspector { | |
108 private final File hgDir; | |
109 private FileOutputStream indexFile; | |
110 private final PathRewrite storagePathHelper; | |
111 | |
112 private final TreeMap<Nodeid, Integer> changelogIndexes = new TreeMap<Nodeid, Integer>(); | |
113 private boolean collectChangelogIndexes = false; | |
114 | |
115 private int base = -1; | |
116 private long offset = 0; | |
117 private DataAccess prevRevContent; | |
118 private final DigestHelper dh = new DigestHelper(); | |
119 private final ArrayList<Nodeid> revisionSequence = new ArrayList<Nodeid>(); // last visited nodes first | |
120 | |
121 private final LinkedList<String> fncacheFiles = new LinkedList<String>(); | |
122 | |
123 public WriteDownMate(File destDir) { | |
124 hgDir = new File(destDir, ".hg"); | |
125 Internals i = new Internals(); | |
126 i.setStorageConfig(1, STORE | FNCACHE | DOTENCODE); | |
127 storagePathHelper = i.buildDataFilesHelper(); | |
128 } | |
129 | |
130 public void initEmptyRepository() throws IOException { | |
131 hgDir.mkdir(); | |
132 FileOutputStream requiresFile = new FileOutputStream(new File(hgDir, "requires")); | |
133 requiresFile.write("revlogv1\nstore\nfncache\ndotencode\n".getBytes()); | |
134 requiresFile.close(); | |
135 new File(hgDir, "store").mkdir(); // with that, hg verify says ok. | |
136 } | |
137 | |
138 public void complete() throws IOException { | |
139 FileOutputStream fncacheFile = new FileOutputStream(new File(hgDir, "store/fncache")); | |
140 for (String s : fncacheFiles) { | |
141 fncacheFile.write(s.getBytes()); | |
142 fncacheFile.write(0x0A); // http://mercurial.selenic.com/wiki/fncacheRepoFormat | |
143 } | |
144 fncacheFile.close(); | |
145 } | |
146 | |
147 public void changelogStart() { | |
148 try { | |
149 base = -1; | |
150 offset = 0; | |
151 revisionSequence.clear(); | |
152 indexFile = new FileOutputStream(new File(hgDir, "store/00changelog.i")); | |
153 collectChangelogIndexes = true; | |
154 } catch (IOException ex) { | |
155 throw new HgBadStateException(ex); | |
156 } | |
157 } | |
158 | |
159 public void changelogEnd() { | |
160 try { | |
161 if (prevRevContent != null) { | |
162 prevRevContent.done(); | |
163 prevRevContent = null; | |
164 } | |
165 collectChangelogIndexes = false; | |
166 indexFile.close(); | |
167 indexFile = null; | |
168 } catch (IOException ex) { | |
169 throw new HgBadStateException(ex); | |
170 } | |
171 } | |
172 | |
173 public void manifestStart() { | |
174 try { | |
175 base = -1; | |
176 offset = 0; | |
177 revisionSequence.clear(); | |
178 indexFile = new FileOutputStream(new File(hgDir, "store/00manifest.i")); | |
179 } catch (IOException ex) { | |
180 throw new HgBadStateException(ex); | |
181 } | |
182 } | |
183 | |
184 public void manifestEnd() { | |
185 try { | |
186 if (prevRevContent != null) { | |
187 prevRevContent.done(); | |
188 prevRevContent = null; | |
189 } | |
190 indexFile.close(); | |
191 indexFile = null; | |
192 } catch (IOException ex) { | |
193 throw new HgBadStateException(ex); | |
194 } | |
195 } | |
196 | |
197 public void fileStart(String name) { | |
198 try { | |
199 base = -1; | |
200 offset = 0; | |
201 revisionSequence.clear(); | |
202 fncacheFiles.add("data/" + name + ".i"); // FIXME this is pure guess, | |
203 // need to investigate more how filenames are kept in fncache | |
204 File file = new File(hgDir, storagePathHelper.rewrite(name)); | |
205 file.getParentFile().mkdirs(); | |
206 indexFile = new FileOutputStream(file); | |
207 } catch (IOException ex) { | |
208 throw new HgBadStateException(ex); | |
209 } | |
210 } | |
211 | |
212 public void fileEnd(String name) { | |
213 try { | |
214 if (prevRevContent != null) { | |
215 prevRevContent.done(); | |
216 prevRevContent = null; | |
217 } | |
218 indexFile.close(); | |
219 indexFile = null; | |
220 } catch (IOException ex) { | |
221 throw new HgBadStateException(ex); | |
222 } | |
223 } | |
224 | |
225 private int knownRevision(Nodeid p) { | |
226 if (NULL.equals(p)) { | |
227 return -1; | |
228 } else { | |
229 for (int i = revisionSequence.size() - 1; i >= 0; i--) { | |
230 if (revisionSequence.get(i).equals(p)) { | |
231 return i; | |
232 } | |
233 } | |
234 } | |
235 throw new HgBadStateException(String.format("Can't find index of %s", p.shortNotation())); | |
236 } | |
237 | |
238 public boolean element(GroupElement ge) { | |
239 try { | |
240 assert indexFile != null; | |
241 boolean writeComplete = false; | |
242 Nodeid p1 = ge.firstParent(); | |
243 Nodeid p2 = ge.secondParent(); | |
244 if (NULL.equals(p1) && NULL.equals(p2) /* or forced flag, does REVIDX_PUNCHED_FLAG indicate that? */) { | |
245 prevRevContent = new ByteArrayDataAccess(new byte[0]); | |
246 writeComplete = true; | |
247 } | |
248 byte[] content = ge.apply(prevRevContent); | |
249 byte[] calculated = dh.sha1(p1, p2, content).asBinary(); | |
250 final Nodeid node = ge.node(); | |
251 if (!node.equalsTo(calculated)) { | |
252 throw new HgBadStateException("Checksum failed"); | |
253 } | |
254 final int link; | |
255 if (collectChangelogIndexes) { | |
256 changelogIndexes.put(node, revisionSequence.size()); | |
257 link = revisionSequence.size(); | |
258 } else { | |
259 Integer csRev = changelogIndexes.get(ge.cset()); | |
260 if (csRev == null) { | |
261 throw new HgBadStateException(String.format("Changelog doesn't contain revision %s", ge.cset().shortNotation())); | |
262 } | |
263 link = csRev.intValue(); | |
264 } | |
265 final int p1Rev = knownRevision(p1), p2Rev = knownRevision(p2); | |
266 DataAccess patchContent = ge.rawData(); | |
267 writeComplete = writeComplete || patchContent.length() >= (/* 3/4 of actual */content.length - (content.length >>> 2)); | |
268 if (writeComplete) { | |
269 base = revisionSequence.size(); | |
270 } | |
271 final byte[] sourceData = writeComplete ? content : patchContent.byteArray(); | |
272 final byte[] data; | |
273 ByteArrayOutputStream bos = new ByteArrayOutputStream(content.length); | |
274 DeflaterOutputStream dos = new DeflaterOutputStream(bos); | |
275 dos.write(sourceData); | |
276 dos.close(); | |
277 final byte[] compressedData = bos.toByteArray(); | |
278 dos = null; | |
279 bos = null; | |
280 final Byte dataPrefix; | |
281 if (compressedData.length >= (sourceData.length - (sourceData.length >>> 2))) { | |
282 // compression wasn't too effective, | |
283 data = sourceData; | |
284 dataPrefix = 'u'; | |
285 } else { | |
286 data = compressedData; | |
287 dataPrefix = null; | |
288 } | |
289 | |
290 ByteBuffer header = ByteBuffer.allocate(64 /* REVLOGV1_RECORD_SIZE */); | |
291 if (offset == 0) { | |
292 final int INLINEDATA = 1 << 16; | |
293 header.putInt(1 /* RevlogNG */ | INLINEDATA); | |
294 header.putInt(0); | |
295 } else { | |
296 header.putLong(offset << 16); | |
297 } | |
298 final int compressedLen = data.length + (dataPrefix == null ? 0 : 1); | |
299 header.putInt(compressedLen); | |
300 header.putInt(content.length); | |
301 header.putInt(base); | |
302 header.putInt(link); | |
303 header.putInt(p1Rev); | |
304 header.putInt(p2Rev); | |
305 header.put(node.toByteArray()); | |
306 // assume 12 bytes left are zeros | |
307 indexFile.write(header.array()); | |
308 if (dataPrefix != null) { | |
309 indexFile.write(dataPrefix.byteValue()); | |
310 } | |
311 indexFile.write(data); | |
312 // | |
313 offset += compressedLen; | |
314 revisionSequence.add(node); | |
315 prevRevContent.done(); | |
316 prevRevContent = new ByteArrayDataAccess(content); | |
317 } catch (IOException ex) { | |
318 throw new HgBadStateException(ex); | |
319 } | |
320 return true; | |
321 } | |
322 } | 56 } |
323 } | 57 } |