1 /*
2 * Copyright (C) 2008-2010, Google Inc.
3 * and other copyright owners as documented in the project's IP log.
4 *
5 * This program and the accompanying materials are made available
6 * under the terms of the Eclipse Distribution License v1.0 which
7 * accompanies this distribution, is reproduced below, and is
8 * available at http://www.eclipse.org/org/documents/edl-v10.php
9 *
10 * All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or
13 * without modification, are permitted provided that the following
14 * conditions are met:
15 *
16 * - Redistributions of source code must retain the above copyright
17 * notice, this list of conditions and the following disclaimer.
18 *
19 * - Redistributions in binary form must reproduce the above
20 * copyright notice, this list of conditions and the following
21 * disclaimer in the documentation and/or other materials provided
22 * with the distribution.
23 *
24 * - Neither the name of the Eclipse Foundation, Inc. nor the
25 * names of its contributors may be used to endorse or promote
26 * products derived from this software without specific prior
27 * written permission.
28 *
29 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42 */
43
44 package org.eclipse.jgit.transport;
45
46 import java.io.IOException;
47 import java.io.OutputStream;
48 import java.io.OutputStreamWriter;
49 import java.io.Writer;
50 import java.text.MessageFormat;
51 import java.util.HashSet;
52 import java.util.Map;
53 import java.util.Set;
54 import java.util.TreeMap;
55
56 import org.eclipse.jgit.internal.JGitText;
57 import org.eclipse.jgit.internal.storage.pack.PackWriter;
58 import org.eclipse.jgit.lib.AnyObjectId;
59 import org.eclipse.jgit.lib.Constants;
60 import org.eclipse.jgit.lib.ObjectId;
61 import org.eclipse.jgit.lib.ObjectReader;
62 import org.eclipse.jgit.lib.ProgressMonitor;
63 import org.eclipse.jgit.lib.Ref;
64 import org.eclipse.jgit.lib.Repository;
65 import org.eclipse.jgit.revwalk.RevCommit;
66 import org.eclipse.jgit.storage.pack.PackConfig;
67
68 /**
69 * Creates a Git bundle file, for sneaker-net transport to another system.
70 * <p>
71 * Bundles generated by this class can be later read in from a file URI using
72 * the bundle transport, or from an application controlled buffer by the more
73 * generic {@link org.eclipse.jgit.transport.TransportBundleStream}.
74 * <p>
75 * Applications creating bundles need to call one or more <code>include</code>
76 * calls to reflect which objects should be available as refs in the bundle for
77 * the other side to fetch. At least one include is required to create a valid
78 * bundle file, and duplicate names are not permitted.
79 * <p>
80 * Optional <code>assume</code> calls can be made to declare commits which the
81 * recipient must have in order to fetch from the bundle file. Objects reachable
82 * from these assumed commits can be used as delta bases in order to reduce the
83 * overall bundle size.
84 */
85 public class BundleWriter {
86 private final Repository db;
87
88 private final ObjectReader reader;
89
90 private final Map<String, ObjectId> include;
91
92 private final Set<RevCommit> assume;
93
94 private final Set<ObjectId> tagTargets;
95
96 private PackConfig packConfig;
97
98 private ObjectCountCallback callback;
99
100 /**
101 * Create a writer for a bundle.
102 *
103 * @param repo
104 * repository where objects are stored.
105 */
106 public BundleWriter(Repository repo) {
107 db = repo;
108 reader = null;
109 include = new TreeMap<>();
110 assume = new HashSet<>();
111 tagTargets = new HashSet<>();
112 }
113
114 /**
115 * Create a writer for a bundle.
116 *
117 * @param or
118 * reader for reading objects. Will be closed at the end of {@link
119 * #writeBundle(ProgressMonitor, OutputStream)}, but readers may be
120 * reused after closing.
121 * @since 4.8
122 */
123 public BundleWriter(ObjectReader or) {
124 db = null;
125 reader = or;
126 include = new TreeMap<>();
127 assume = new HashSet<>();
128 tagTargets = new HashSet<>();
129 }
130
131 /**
132 * Set the configuration used by the pack generator.
133 *
134 * @param pc
135 * configuration controlling packing parameters. If null the
136 * source repository's settings will be used, or the default
137 * settings if constructed without a repo.
138 */
139 public void setPackConfig(PackConfig pc) {
140 this.packConfig = pc;
141 }
142
143 /**
144 * Include an object (and everything reachable from it) in the bundle.
145 *
146 * @param name
147 * name the recipient can discover this object as from the
148 * bundle's list of advertised refs . The name must be a valid
149 * ref format and must not have already been included in this
150 * bundle writer.
151 * @param id
152 * object to pack. Multiple refs may point to the same object.
153 */
154 public void include(final String name, final AnyObjectId id) {
155 boolean validRefName = Repository.isValidRefName(name) || Constants.HEAD.equals(name);
156 if (!validRefName)
157 throw new IllegalArgumentException(MessageFormat.format(JGitText.get().invalidRefName, name));
158 if (include.containsKey(name))
159 throw new IllegalStateException(JGitText.get().duplicateRef + name);
160 include.put(name, id.toObjectId());
161 }
162
163 /**
164 * Include a single ref (a name/object pair) in the bundle.
165 * <p>
166 * This is a utility function for:
167 * <code>include(r.getName(), r.getObjectId())</code>.
168 *
169 * @param r
170 * the ref to include.
171 */
172 public void include(final Ref r) {
173 include(r.getName(), r.getObjectId());
174
175 if (r.getPeeledObjectId() != null)
176 tagTargets.add(r.getPeeledObjectId());
177
178 else if (r.getObjectId() != null
179 && r.getName().startsWith(Constants.R_HEADS))
180 tagTargets.add(r.getObjectId());
181 }
182
183 /**
184 * Assume a commit is available on the recipient's side.
185 * <p>
186 * In order to fetch from a bundle the recipient must have any assumed
187 * commit. Each assumed commit is explicitly recorded in the bundle header
188 * to permit the recipient to validate it has these objects.
189 *
190 * @param c
191 * the commit to assume being available. This commit should be
192 * parsed and not disposed in order to maximize the amount of
193 * debugging information available in the bundle stream.
194 */
195 public void assume(final RevCommit c) {
196 if (c != null)
197 assume.add(c);
198 }
199
200 /**
201 * Generate and write the bundle to the output stream.
202 * <p>
203 * This method can only be called once per BundleWriter instance.
204 *
205 * @param monitor
206 * progress monitor to report bundle writing status to.
207 * @param os
208 * the stream the bundle is written to. The stream should be
209 * buffered by the caller. The caller is responsible for closing
210 * the stream.
211 * @throws java.io.IOException
212 * an error occurred reading a local object's data to include in
213 * the bundle, or writing compressed object data to the output
214 * stream.
215 */
216 public void writeBundle(ProgressMonitor monitor, OutputStream os)
217 throws IOException {
218 try (PackWriter packWriter = newPackWriter()) {
219 packWriter.setObjectCountCallback(callback);
220
221 final HashSet<ObjectId> inc = new HashSet<>();
222 final HashSet<ObjectId> exc = new HashSet<>();
223 inc.addAll(include.values());
224 for (final RevCommit r : assume)
225 exc.add(r.getId());
226 packWriter.setIndexDisabled(true);
227 packWriter.setDeltaBaseAsOffset(true);
228 packWriter.setThin(exc.size() > 0);
229 packWriter.setReuseValidatingObjects(false);
230 if (exc.size() == 0)
231 packWriter.setTagTargets(tagTargets);
232 packWriter.preparePack(monitor, inc, exc);
233
234 final Writer w = new OutputStreamWriter(os, Constants.CHARSET);
235 w.write(TransportBundle.V2_BUNDLE_SIGNATURE);
236 w.write('\n');
237
238 final char[] tmp = new char[Constants.OBJECT_ID_STRING_LENGTH];
239 for (final RevCommit a : assume) {
240 w.write('-');
241 a.copyTo(tmp, w);
242 if (a.getRawBuffer() != null) {
243 w.write(' ');
244 w.write(a.getShortMessage());
245 }
246 w.write('\n');
247 }
248 for (final Map.Entry<String, ObjectId> e : include.entrySet()) {
249 e.getValue().copyTo(tmp, w);
250 w.write(' ');
251 w.write(e.getKey());
252 w.write('\n');
253 }
254
255 w.write('\n');
256 w.flush();
257 packWriter.writePack(monitor, monitor, os);
258 }
259 }
260
261 private PackWriter newPackWriter() {
262 PackConfig pc = packConfig;
263 if (pc == null) {
264 pc = db != null ? new PackConfig(db) : new PackConfig();
265 }
266 return new PackWriter(pc, reader != null ? reader : db.newObjectReader());
267 }
268
269 /**
270 * Set the {@link org.eclipse.jgit.transport.ObjectCountCallback}.
271 * <p>
272 * It should be set before calling
273 * {@link #writeBundle(ProgressMonitor, OutputStream)}.
274 * <p>
275 * This callback will be passed on to
276 * {@link org.eclipse.jgit.internal.storage.pack.PackWriter#setObjectCountCallback}.
277 *
278 * @param callback
279 * the callback to set
280 * @return this object for chaining.
281 * @since 4.1
282 */
283 public BundleWriter setObjectCountCallback(ObjectCountCallback callback) {
284 this.callback = callback;
285 return this;
286 }
287 }