View Javadoc
1   /*
2    * Copyright (C) 2008-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.transport;
12  
13  import static java.nio.charset.StandardCharsets.UTF_8;
14  
15  import java.io.IOException;
16  import java.io.OutputStream;
17  import java.io.OutputStreamWriter;
18  import java.io.Writer;
19  import java.text.MessageFormat;
20  import java.util.ArrayList;
21  import java.util.Collection;
22  import java.util.HashSet;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Set;
26  import java.util.TreeMap;
27  
28  import org.eclipse.jgit.internal.JGitText;
29  import org.eclipse.jgit.internal.storage.pack.CachedPack;
30  import org.eclipse.jgit.internal.storage.pack.PackWriter;
31  import org.eclipse.jgit.lib.AnyObjectId;
32  import org.eclipse.jgit.lib.Constants;
33  import org.eclipse.jgit.lib.ObjectId;
34  import org.eclipse.jgit.lib.ObjectReader;
35  import org.eclipse.jgit.lib.ProgressMonitor;
36  import org.eclipse.jgit.lib.Ref;
37  import org.eclipse.jgit.lib.Repository;
38  import org.eclipse.jgit.revwalk.RevCommit;
39  import org.eclipse.jgit.storage.pack.PackConfig;
40  
41  /**
42   * Creates a Git bundle file, for sneaker-net transport to another system.
43   * <p>
44   * Bundles generated by this class can be later read in from a file URI using
45   * the bundle transport, or from an application controlled buffer by the more
46   * generic {@link org.eclipse.jgit.transport.TransportBundleStream}.
47   * <p>
48   * Applications creating bundles need to call one or more <code>include</code>
49   * calls to reflect which objects should be available as refs in the bundle for
50   * the other side to fetch. At least one include is required to create a valid
51   * bundle file, and duplicate names are not permitted.
52   * <p>
53   * Optional <code>assume</code> calls can be made to declare commits which the
54   * recipient must have in order to fetch from the bundle file. Objects reachable
55   * from these assumed commits can be used as delta bases in order to reduce the
56   * overall bundle size.
57   */
58  public class BundleWriter {
59  	private final Repository db;
60  
61  	private final ObjectReader reader;
62  
63  	private final Map<String, ObjectId> include;
64  
65  	private final Set<RevCommit> assume;
66  
67  	private final Set<ObjectId> tagTargets;
68  
69  	private final List<CachedPack> cachedPacks = new ArrayList<>();
70  
71  	private PackConfig packConfig;
72  
73  	private ObjectCountCallback callback;
74  
75  	/**
76  	 * Create a writer for a bundle.
77  	 *
78  	 * @param repo
79  	 *            repository where objects are stored.
80  	 */
81  	public BundleWriter(Repository repo) {
82  		db = repo;
83  		reader = null;
84  		include = new TreeMap<>();
85  		assume = new HashSet<>();
86  		tagTargets = new HashSet<>();
87  	}
88  
89  	/**
90  	 * Create a writer for a bundle.
91  	 *
92  	 * @param or
93  	 *            reader for reading objects. Will be closed at the end of {@link
94  	 *            #writeBundle(ProgressMonitor, OutputStream)}, but readers may be
95  	 *            reused after closing.
96  	 * @since 4.8
97  	 */
98  	public BundleWriter(ObjectReader or) {
99  		db = null;
100 		reader = or;
101 		include = new TreeMap<>();
102 		assume = new HashSet<>();
103 		tagTargets = new HashSet<>();
104 	}
105 
106 	/**
107 	 * Set the configuration used by the pack generator.
108 	 *
109 	 * @param pc
110 	 *            configuration controlling packing parameters. If null the
111 	 *            source repository's settings will be used, or the default
112 	 *            settings if constructed without a repo.
113 	 */
114 	public void setPackConfig(PackConfig pc) {
115 		this.packConfig = pc;
116 	}
117 
118 	/**
119 	 * Include an object (and everything reachable from it) in the bundle.
120 	 *
121 	 * @param name
122 	 *            name the recipient can discover this object as from the
123 	 *            bundle's list of advertised refs . The name must be a valid
124 	 *            ref format and must not have already been included in this
125 	 *            bundle writer.
126 	 * @param id
127 	 *            object to pack. Multiple refs may point to the same object.
128 	 */
129 	public void include(String name, AnyObjectId id) {
130 		boolean validRefName = Repository.isValidRefName(name) || Constants.HEAD.equals(name);
131 		if (!validRefName)
132 			throw new IllegalArgumentException(MessageFormat.format(JGitText.get().invalidRefName, name));
133 		if (include.containsKey(name))
134 			throw new IllegalStateException(JGitText.get().duplicateRef + name);
135 		include.put(name, id.toObjectId());
136 	}
137 
138 	/**
139 	 * Include a single ref (a name/object pair) in the bundle.
140 	 * <p>
141 	 * This is a utility function for:
142 	 * <code>include(r.getName(), r.getObjectId())</code>.
143 	 *
144 	 * @param r
145 	 *            the ref to include.
146 	 */
147 	public void include(Ref r) {
148 		include(r.getName(), r.getObjectId());
149 
150 		if (r.getPeeledObjectId() != null)
151 			tagTargets.add(r.getPeeledObjectId());
152 
153 		else if (r.getObjectId() != null
154 				&& r.getName().startsWith(Constants.R_HEADS))
155 			tagTargets.add(r.getObjectId());
156 	}
157 
158 	/**
159 	 * Add objects to the bundle file.
160 	 *
161 	 * <p>
162 	 * When this method is used, object traversal is disabled and specified pack
163 	 * files are directly saved to the Git bundle file.
164 	 *
165 	 * <p>
166 	 * Unlike {@link #include}, this doesn't affect the refs. Even if the
167 	 * objects are not reachable from any ref, they will be included in the
168 	 * bundle file.
169 	 *
170 	 * @param c
171 	 *            pack to include
172 	 * @since 5.9
173 	 */
174 	public void addObjectsAsIs(Collection<? extends CachedPack> c) {
175 		cachedPacks.addAll(c);
176 	}
177 
178 	/**
179 	 * Assume a commit is available on the recipient's side.
180 	 * <p>
181 	 * In order to fetch from a bundle the recipient must have any assumed
182 	 * commit. Each assumed commit is explicitly recorded in the bundle header
183 	 * to permit the recipient to validate it has these objects.
184 	 *
185 	 * @param c
186 	 *            the commit to assume being available. This commit should be
187 	 *            parsed and not disposed in order to maximize the amount of
188 	 *            debugging information available in the bundle stream.
189 	 */
190 	public void assume(RevCommit c) {
191 		if (c != null)
192 			assume.add(c);
193 	}
194 
195 	/**
196 	 * Generate and write the bundle to the output stream.
197 	 * <p>
198 	 * This method can only be called once per BundleWriter instance.
199 	 *
200 	 * @param monitor
201 	 *            progress monitor to report bundle writing status to.
202 	 * @param os
203 	 *            the stream the bundle is written to. The stream should be
204 	 *            buffered by the caller. The caller is responsible for closing
205 	 *            the stream.
206 	 * @throws java.io.IOException
207 	 *             an error occurred reading a local object's data to include in
208 	 *             the bundle, or writing compressed object data to the output
209 	 *             stream.
210 	 */
211 	public void writeBundle(ProgressMonitor monitor, OutputStream os)
212 			throws IOException {
213 		try (PackWriter packWriter = newPackWriter()) {
214 			packWriter.setObjectCountCallback(callback);
215 
216 			packWriter.setIndexDisabled(true);
217 			packWriter.setDeltaBaseAsOffset(true);
218 			packWriter.setReuseValidatingObjects(false);
219 			if (cachedPacks.isEmpty()) {
220 				HashSet<ObjectId> inc = new HashSet<>();
221 				HashSet<ObjectId> exc = new HashSet<>();
222 				inc.addAll(include.values());
223 				for (RevCommit r : assume) {
224 					exc.add(r.getId());
225 				}
226 				if (exc.isEmpty()) {
227 					packWriter.setTagTargets(tagTargets);
228 				}
229 				packWriter.setThin(!exc.isEmpty());
230 				packWriter.preparePack(monitor, inc, exc);
231 			} else {
232 				packWriter.preparePack(cachedPacks);
233 			}
234 
235 			final Writer w = new OutputStreamWriter(os, UTF_8);
236 			w.write(TransportBundle.V2_BUNDLE_SIGNATURE);
237 			w.write('\n');
238 
239 			final char[] tmp = new char[Constants.OBJECT_ID_STRING_LENGTH];
240 			for (RevCommit a : assume) {
241 				w.write('-');
242 				a.copyTo(tmp, w);
243 				if (a.getRawBuffer() != null) {
244 					w.write(' ');
245 					w.write(a.getShortMessage());
246 				}
247 				w.write('\n');
248 			}
249 			for (Map.Entry<String, ObjectId> e : include.entrySet()) {
250 				e.getValue().copyTo(tmp, w);
251 				w.write(' ');
252 				w.write(e.getKey());
253 				w.write('\n');
254 			}
255 
256 			w.write('\n');
257 			w.flush();
258 			packWriter.writePack(monitor, monitor, os);
259 		}
260 	}
261 
262 	private PackWriter newPackWriter() {
263 		PackConfig pc = packConfig;
264 		if (pc == null) {
265 			pc = db != null ? new PackConfig(db) : new PackConfig();
266 		}
267 		return new PackWriter(pc, reader != null ? reader : db.newObjectReader());
268 	}
269 
270 	/**
271 	 * Set the {@link org.eclipse.jgit.transport.ObjectCountCallback}.
272 	 * <p>
273 	 * It should be set before calling
274 	 * {@link #writeBundle(ProgressMonitor, OutputStream)}.
275 	 * <p>
276 	 * This callback will be passed on to
277 	 * {@link org.eclipse.jgit.internal.storage.pack.PackWriter#setObjectCountCallback}.
278 	 *
279 	 * @param callback
280 	 *            the callback to set
281 	 * @return this object for chaining.
282 	 * @since 4.1
283 	 */
284 	public BundleWriter setObjectCountCallback(ObjectCountCallback callback) {
285 		this.callback = callback;
286 		return this;
287 	}
288 }