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