Simplify the PackWriter.preparePack() API
[jgit.git] / org.spearce.jgit / src / org / spearce / jgit / transport / UploadPack.java
blob45d57b3774324614ca479c9b21fff5832602e8b9
1 /*
2 * Copyright (C) 2008, Google Inc.
4 * All rights reserved.
6 * Redistribution and use in source and binary forms, with or
7 * without modification, are permitted provided that the following
8 * conditions are met:
10 * - Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
13 * - Redistributions in binary form must reproduce the above
14 * copyright notice, this list of conditions and the following
15 * disclaimer in the documentation and/or other materials provided
16 * with the distribution.
18 * - Neither the name of the Git Development Community nor the
19 * names of its contributors may be used to endorse or promote
20 * products derived from this software without specific prior
21 * written permission.
23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
24 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
25 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
26 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
28 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
30 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
33 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
34 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
35 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 package org.spearce.jgit.transport;
40 import java.io.BufferedOutputStream;
41 import java.io.EOFException;
42 import java.io.IOException;
43 import java.io.InputStream;
44 import java.io.OutputStream;
45 import java.util.ArrayList;
46 import java.util.HashSet;
47 import java.util.Iterator;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.Set;
52 import org.spearce.jgit.errors.PackProtocolException;
53 import org.spearce.jgit.lib.Constants;
54 import org.spearce.jgit.lib.NullProgressMonitor;
55 import org.spearce.jgit.lib.ObjectId;
56 import org.spearce.jgit.lib.PackWriter;
57 import org.spearce.jgit.lib.ProgressMonitor;
58 import org.spearce.jgit.lib.Ref;
59 import org.spearce.jgit.lib.RefComparator;
60 import org.spearce.jgit.lib.Repository;
61 import org.spearce.jgit.revwalk.RevCommit;
62 import org.spearce.jgit.revwalk.RevFlag;
63 import org.spearce.jgit.revwalk.RevFlagSet;
64 import org.spearce.jgit.revwalk.RevObject;
65 import org.spearce.jgit.revwalk.RevTag;
66 import org.spearce.jgit.revwalk.RevWalk;
68 /**
69 * Implements the server side of a fetch connection, transmitting objects.
71 public class UploadPack {
72 static final String OPTION_INCLUDE_TAG = BasePackFetchConnection.OPTION_INCLUDE_TAG;
74 static final String OPTION_MULTI_ACK = BasePackFetchConnection.OPTION_MULTI_ACK;
76 static final String OPTION_THIN_PACK = BasePackFetchConnection.OPTION_THIN_PACK;
78 static final String OPTION_SIDE_BAND = BasePackFetchConnection.OPTION_SIDE_BAND;
80 static final String OPTION_SIDE_BAND_64K = BasePackFetchConnection.OPTION_SIDE_BAND_64K;
82 static final String OPTION_OFS_DELTA = BasePackFetchConnection.OPTION_OFS_DELTA;
84 static final String OPTION_NO_PROGRESS = BasePackFetchConnection.OPTION_NO_PROGRESS;
86 /** Database we read the objects from. */
87 private final Repository db;
89 /** Revision traversal support over {@link #db}. */
90 private final RevWalk walk;
92 private InputStream rawIn;
94 private OutputStream rawOut;
96 private PacketLineIn pckIn;
98 private PacketLineOut pckOut;
100 /** The refs we advertised as existing at the start of the connection. */
101 private Map<String, Ref> refs;
103 /** Capabilities requested by the client. */
104 private final Set<String> options = new HashSet<String>();
106 /** Objects the client wants to obtain. */
107 private final List<RevObject> wantAll = new ArrayList<RevObject>();
109 /** Objects the client wants to obtain. */
110 private final List<RevCommit> wantCommits = new ArrayList<RevCommit>();
112 /** Objects on both sides, these don't have to be sent. */
113 private final List<RevObject> commonBase = new ArrayList<RevObject>();
115 /** Marked on objects we sent in our advertisement list. */
116 private final RevFlag ADVERTISED;
118 /** Marked on objects the client has asked us to give them. */
119 private final RevFlag WANT;
121 /** Marked on objects both we and the client have. */
122 private final RevFlag PEER_HAS;
124 /** Marked on objects in {@link #commonBase}. */
125 private final RevFlag COMMON;
127 private final RevFlagSet SAVE;
129 private boolean multiAck;
132 * Create a new pack upload for an open repository.
134 * @param copyFrom
135 * the source repository.
137 public UploadPack(final Repository copyFrom) {
138 db = copyFrom;
139 walk = new RevWalk(db);
141 ADVERTISED = walk.newFlag("ADVERTISED");
142 WANT = walk.newFlag("WANT");
143 PEER_HAS = walk.newFlag("PEER_HAS");
144 COMMON = walk.newFlag("COMMON");
145 walk.carry(PEER_HAS);
147 SAVE = new RevFlagSet();
148 SAVE.add(ADVERTISED);
149 SAVE.add(WANT);
150 SAVE.add(PEER_HAS);
153 /** @return the repository this receive completes into. */
154 public final Repository getRepository() {
155 return db;
158 /** @return the RevWalk instance used by this connection. */
159 public final RevWalk getRevWalk() {
160 return walk;
164 * Execute the upload task on the socket.
166 * @param input
167 * raw input to read client commands from. Caller must ensure the
168 * input is buffered, otherwise read performance may suffer.
169 * @param output
170 * response back to the Git network client, to write the pack
171 * data onto. Caller must ensure the output is buffered,
172 * otherwise write performance may suffer.
173 * @param messages
174 * secondary "notice" channel to send additional messages out
175 * through. When run over SSH this should be tied back to the
176 * standard error channel of the command execution. For most
177 * other network connections this should be null.
178 * @throws IOException
180 public void upload(final InputStream input, final OutputStream output,
181 final OutputStream messages) throws IOException {
182 rawIn = input;
183 rawOut = output;
185 pckIn = new PacketLineIn(rawIn);
186 pckOut = new PacketLineOut(rawOut);
187 service();
190 private void service() throws IOException {
191 sendAdvertisedRefs();
192 recvWants();
193 if (wantAll.isEmpty())
194 return;
195 multiAck = options.contains(OPTION_MULTI_ACK);
196 negotiate();
197 sendPack();
200 private void sendAdvertisedRefs() throws IOException {
201 refs = db.getAllRefs();
203 final StringBuilder m = new StringBuilder(100);
204 final char[] idtmp = new char[2 * Constants.OBJECT_ID_LENGTH];
205 final Iterator<Ref> i = RefComparator.sort(refs.values()).iterator();
206 if (i.hasNext()) {
207 final Ref r = i.next();
208 final RevObject o = safeParseAny(r.getObjectId());
209 if (o != null) {
210 advertise(m, idtmp, o, r.getOrigName());
211 m.append('\0');
212 m.append(' ');
213 m.append(OPTION_INCLUDE_TAG);
214 m.append(' ');
215 m.append(OPTION_MULTI_ACK);
216 m.append(' ');
217 m.append(OPTION_OFS_DELTA);
218 m.append(' ');
219 m.append(OPTION_SIDE_BAND);
220 m.append(' ');
221 m.append(OPTION_SIDE_BAND_64K);
222 m.append(' ');
223 m.append(OPTION_THIN_PACK);
224 m.append(' ');
225 m.append(OPTION_NO_PROGRESS);
226 m.append(' ');
227 writeAdvertisedRef(m);
228 if (o instanceof RevTag)
229 writeAdvertisedTag(m, idtmp, o, r.getName());
232 while (i.hasNext()) {
233 final Ref r = i.next();
234 final RevObject o = safeParseAny(r.getObjectId());
235 if (o != null) {
236 advertise(m, idtmp, o, r.getOrigName());
237 writeAdvertisedRef(m);
238 if (o instanceof RevTag)
239 writeAdvertisedTag(m, idtmp, o, r.getName());
242 pckOut.end();
245 private RevObject safeParseAny(final ObjectId id) {
246 try {
247 return walk.parseAny(id);
248 } catch (IOException e) {
249 return null;
253 private void advertise(final StringBuilder m, final char[] idtmp,
254 final RevObject o, final String name) {
255 o.add(ADVERTISED);
256 m.setLength(0);
257 o.getId().copyTo(idtmp, m);
258 m.append(' ');
259 m.append(name);
262 private void writeAdvertisedRef(final StringBuilder m) throws IOException {
263 m.append('\n');
264 pckOut.writeString(m.toString());
267 private void writeAdvertisedTag(final StringBuilder m, final char[] idtmp,
268 final RevObject tag, final String name) throws IOException {
269 RevObject o = tag;
270 while (o instanceof RevTag) {
271 // Fully unwrap here so later on we have these already parsed.
272 try {
273 walk.parse(((RevTag) o).getObject());
274 } catch (IOException err) {
275 return;
277 o = ((RevTag) o).getObject();
278 o.add(ADVERTISED);
280 advertise(m, idtmp, ((RevTag) tag).getObject(), name + "^{}");
281 writeAdvertisedRef(m);
284 private void recvWants() throws IOException {
285 boolean isFirst = true;
286 for (;; isFirst = false) {
287 String line;
288 try {
289 line = pckIn.readString();
290 } catch (EOFException eof) {
291 if (isFirst)
292 break;
293 throw eof;
296 if (line.length() == 0)
297 break;
298 if (!line.startsWith("want ") || line.length() < 45)
299 throw new PackProtocolException("expected want; got " + line);
301 if (isFirst) {
302 final int sp = line.indexOf(' ', 45);
303 if (sp >= 0) {
304 for (String c : line.substring(sp + 1).split(" "))
305 options.add(c);
306 line = line.substring(0, sp);
310 final ObjectId id = ObjectId.fromString(line.substring(5));
311 final RevObject o;
312 try {
313 o = walk.parseAny(id);
314 } catch (IOException e) {
315 throw new PackProtocolException(id.name() + " not valid", e);
317 if (!o.has(ADVERTISED))
318 throw new PackProtocolException(id.name() + " not valid");
319 want(o);
323 private void want(RevObject o) {
324 if (!o.has(WANT)) {
325 o.add(WANT);
326 wantAll.add(o);
328 if (o instanceof RevCommit)
329 wantCommits.add((RevCommit) o);
331 else if (o instanceof RevTag) {
332 do {
333 o = ((RevTag) o).getObject();
334 } while (o instanceof RevTag);
335 if (o instanceof RevCommit)
336 want(o);
341 private void negotiate() throws IOException {
342 ObjectId last = ObjectId.zeroId();
343 for (;;) {
344 String line;
345 try {
346 line = pckIn.readString();
347 } catch (EOFException eof) {
348 throw eof;
351 if (line.length() == 0) {
352 if (commonBase.isEmpty() || multiAck)
353 pckOut.writeString("NAK\n");
354 pckOut.flush();
355 } else if (line.startsWith("have ") && line.length() == 45) {
356 final ObjectId id = ObjectId.fromString(line.substring(5));
357 if (matchHave(id)) {
358 // Both sides have the same object; let the client know.
360 if (multiAck) {
361 last = id;
362 pckOut.writeString("ACK " + id.name() + " continue\n");
363 } else if (commonBase.size() == 1)
364 pckOut.writeString("ACK " + id.name() + "\n");
365 } else {
366 // They have this object; we don't.
368 if (multiAck && okToGiveUp())
369 pckOut.writeString("ACK " + id.name() + " continue\n");
372 } else if (line.equals("done")) {
373 if (commonBase.isEmpty())
374 pckOut.writeString("NAK\n");
376 else if (multiAck)
377 pckOut.writeString("ACK " + last.name() + "\n");
378 break;
380 } else {
381 throw new PackProtocolException("expected have; got " + line);
386 private boolean matchHave(final ObjectId id) {
387 final RevObject o;
388 try {
389 o = walk.parseAny(id);
390 } catch (IOException err) {
391 return false;
394 if (!o.has(PEER_HAS)) {
395 o.add(PEER_HAS);
396 if (o instanceof RevCommit)
397 ((RevCommit) o).carry(PEER_HAS);
398 if (!o.has(COMMON)) {
399 o.add(COMMON);
400 commonBase.add(o);
403 return true;
406 private boolean okToGiveUp() throws PackProtocolException {
407 if (commonBase.isEmpty())
408 return false;
410 try {
411 for (final Iterator<RevCommit> i = wantCommits.iterator(); i
412 .hasNext();) {
413 final RevCommit want = i.next();
414 if (wantSatisfied(want))
415 i.remove();
417 } catch (IOException e) {
418 throw new PackProtocolException("internal revision error", e);
420 return wantCommits.isEmpty();
423 private boolean wantSatisfied(final RevCommit want) throws IOException {
424 walk.resetRetain(SAVE);
425 walk.markStart(want);
426 for (;;) {
427 final RevCommit c = walk.next();
428 if (c == null)
429 break;
430 if (c.has(PEER_HAS)) {
431 if (!c.has(COMMON)) {
432 c.add(COMMON);
433 commonBase.add(c);
435 return true;
437 c.dispose();
439 return false;
442 private void sendPack() throws IOException {
443 final boolean thin = options.contains(OPTION_THIN_PACK);
444 final boolean progress = !options.contains(OPTION_NO_PROGRESS);
445 final boolean sideband = options.contains(OPTION_SIDE_BAND)
446 || options.contains(OPTION_SIDE_BAND_64K);
448 ProgressMonitor pm = NullProgressMonitor.INSTANCE;
449 OutputStream packOut = rawOut;
451 if (sideband) {
452 int bufsz = SideBandOutputStream.SMALL_BUF;
453 if (options.contains(OPTION_SIDE_BAND_64K))
454 bufsz = SideBandOutputStream.MAX_BUF;
455 bufsz -= SideBandOutputStream.HDR_SIZE;
457 packOut = new BufferedOutputStream(new SideBandOutputStream(
458 SideBandOutputStream.CH_DATA, pckOut), bufsz);
460 if (progress)
461 pm = new SideBandProgressMonitor(pckOut);
464 final PackWriter pw;
465 pw = new PackWriter(db, pm, NullProgressMonitor.INSTANCE);
466 pw.setDeltaBaseAsOffset(options.contains(OPTION_OFS_DELTA));
467 pw.setThin(thin);
468 pw.preparePack(wantAll, commonBase);
469 if (options.contains(OPTION_INCLUDE_TAG)) {
470 for (final Ref r : refs.values()) {
471 final RevObject o;
472 try {
473 o = walk.parseAny(r.getObjectId());
474 } catch (IOException e) {
475 continue;
477 if (o.has(WANT) || !(o instanceof RevTag))
478 continue;
479 final RevTag t = (RevTag) o;
480 if (!pw.willInclude(t) && pw.willInclude(t.getObject()))
481 pw.addObject(t);
484 pw.writePack(packOut);
486 if (sideband) {
487 packOut.flush();
488 pckOut.end();
489 } else {
490 rawOut.flush();