1 /*
2 * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
3 * Copyright (C) 2006-2007, Robin Rosenberg <robin.rosenberg@dewire.com>
4 * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org>
5 * and other copyright owners as documented in the project's IP log.
6 *
7 * This program and the accompanying materials are made available
8 * under the terms of the Eclipse Distribution License v1.0 which
9 * accompanies this distribution, is reproduced below, and is
10 * available at http://www.eclipse.org/org/documents/edl-v10.php
11 *
12 * All rights reserved.
13 *
14 * Redistribution and use in source and binary forms, with or
15 * without modification, are permitted provided that the following
16 * conditions are met:
17 *
18 * - Redistributions of source code must retain the above copyright
19 * notice, this list of conditions and the following disclaimer.
20 *
21 * - Redistributions in binary form must reproduce the above
22 * copyright notice, this list of conditions and the following
23 * disclaimer in the documentation and/or other materials provided
24 * with the distribution.
25 *
26 * - Neither the name of the Eclipse Foundation, Inc. nor the
27 * names of its contributors may be used to endorse or promote
28 * products derived from this software without specific prior
29 * written permission.
30 *
31 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
32 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
33 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
34 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
35 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
36 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
37 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
38 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
39 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
40 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
41 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
42 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
43 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
44 */
45
46 package org.eclipse.jgit.lib;
47
48 import static java.nio.charset.StandardCharsets.UTF_8;
49
50 import java.io.ByteArrayOutputStream;
51 import java.io.IOException;
52 import java.io.OutputStream;
53 import java.io.OutputStreamWriter;
54 import java.io.UnsupportedEncodingException;
55 import java.nio.charset.Charset;
56 import java.text.MessageFormat;
57 import java.util.List;
58
59 import org.eclipse.jgit.internal.JGitText;
60
61 /**
62 * Mutable builder to construct a commit recording the state of a project.
63 *
64 * Applications should use this object when they need to manually construct a
65 * commit and want precise control over its fields. For a higher level interface
66 * see {@link org.eclipse.jgit.api.CommitCommand}.
67 *
68 * To read a commit object, construct a {@link org.eclipse.jgit.revwalk.RevWalk}
69 * and obtain a {@link org.eclipse.jgit.revwalk.RevCommit} instance by calling
70 * {@link org.eclipse.jgit.revwalk.RevWalk#parseCommit(AnyObjectId)}.
71 */
72 public class CommitBuilder {
73 private static final ObjectIdObjectId[] EMPTY_OBJECTID_LIST = new ObjectId[0];
74
75 private static final byte[] htree = Constants.encodeASCII("tree"); //$NON-NLS-1$
76
77 private static final byte[] hparent = Constants.encodeASCII("parent"); //$NON-NLS-1$
78
79 private static final byte[] hauthor = Constants.encodeASCII("author"); //$NON-NLS-1$
80
81 private static final byte[] hcommitter = Constants.encodeASCII("committer"); //$NON-NLS-1$
82
83 private static final byte[] hgpgsig = Constants.encodeASCII("gpgsig"); //$NON-NLS-1$
84
85 private static final byte[] hencoding = Constants.encodeASCII("encoding"); //$NON-NLS-1$
86
87 private ObjectId treeId;
88
89 private ObjectId[] parentIds;
90
91 private PersonIdent author;
92
93 private PersonIdent committer;
94
95 private GpgSignature gpgSignature;
96
97 private String message;
98
99 private Charset encoding;
100
101 /**
102 * Initialize an empty commit.
103 */
104 public CommitBuilder() {
105 parentIds = EMPTY_OBJECTID_LIST;
106 encoding = UTF_8;
107 }
108
109 /**
110 * Get id of the root tree listing this commit's snapshot.
111 *
112 * @return id of the root tree listing this commit's snapshot.
113 */
114 public ObjectId getTreeId() {
115 return treeId;
116 }
117
118 /**
119 * Set the tree id for this commit object.
120 *
121 * @param id
122 * the tree identity.
123 */
124 public void setTreeId(AnyObjectId id) {
125 treeId = id.copy();
126 }
127
128 /**
129 * Get the author of this commit (who wrote it).
130 *
131 * @return the author of this commit (who wrote it).
132 */
133 public PersonIdent getAuthor() {
134 return author;
135 }
136
137 /**
138 * Set the author (name, email address, and date) of who wrote the commit.
139 *
140 * @param newAuthor
141 * the new author. Should not be null.
142 */
143 public void setAuthor(PersonIdent newAuthor) {
144 author = newAuthor;
145 }
146
147 /**
148 * Get the committer and commit time for this object.
149 *
150 * @return the committer and commit time for this object.
151 */
152 public PersonIdent getCommitter() {
153 return committer;
154 }
155
156 /**
157 * Set the committer and commit time for this object.
158 *
159 * @param newCommitter
160 * the committer information. Should not be null.
161 */
162 public void setCommitter(PersonIdent newCommitter) {
163 committer = newCommitter;
164 }
165
166 /**
167 * Set the GPG signature of this commit.
168 * <p>
169 * Note, the signature set here will change the payload of the commit, i.e.
170 * the output of {@link #build()} will include the signature. Thus, the
171 * typical flow will be:
172 * <ol>
173 * <li>call {@link #build()} without a signature set to obtain payload</li>
174 * <li>create {@link GpgSignature} from payload</li>
175 * <li>set {@link GpgSignature}</li>
176 * </ol>
177 * </p>
178 *
179 * @param newSignature
180 * the signature to set or <code>null</code> to unset
181 * @since 5.3
182 */
183 public void setGpgSignature(GpgSignature newSignature) {
184 gpgSignature = newSignature;
185 }
186
187 /**
188 * Get the GPG signature of this commit.
189 *
190 * @return the GPG signature of this commit, maybe <code>null</code> if the
191 * commit is not to be signed
192 * @since 5.3
193 */
194 public GpgSignature getGpgSignature() {
195 return gpgSignature;
196 }
197
198 /**
199 * Get the ancestors of this commit.
200 *
201 * @return the ancestors of this commit. Never null.
202 */
203 public ObjectId[] getParentIds() {
204 return parentIds;
205 }
206
207 /**
208 * Set the parent of this commit.
209 *
210 * @param newParent
211 * the single parent for the commit.
212 */
213 public void setParentId(AnyObjectId newParent) {
214 parentIds = new ObjectId[] { newParent.copy() };
215 }
216
217 /**
218 * Set the parents of this commit.
219 *
220 * @param parent1
221 * the first parent of this commit. Typically this is the current
222 * value of the {@code HEAD} reference and is thus the current
223 * branch's position in history.
224 * @param parent2
225 * the second parent of this merge commit. Usually this is the
226 * branch being merged into the current branch.
227 */
228 public void setParentIds(AnyObjectId./../../org/eclipse/jgit/lib/AnyObjectId.html#AnyObjectId">AnyObjectId parent1, AnyObjectId parent2) {
229 parentIds = new ObjectId[] { parent1.copy(), parent2.copy() };
230 }
231
232 /**
233 * Set the parents of this commit.
234 *
235 * @param newParents
236 * the entire list of parents for this commit.
237 */
238 public void setParentIds(ObjectId... newParents) {
239 parentIds = new ObjectId[newParents.length];
240 for (int i = 0; i < newParents.length; i++)
241 parentIds[i] = newParents[i].copy();
242 }
243
244 /**
245 * Set the parents of this commit.
246 *
247 * @param newParents
248 * the entire list of parents for this commit.
249 */
250 public void setParentIds(List<? extends AnyObjectId> newParents) {
251 parentIds = new ObjectId[newParents.size()];
252 for (int i = 0; i < newParents.size(); i++)
253 parentIds[i] = newParents.get(i).copy();
254 }
255
256 /**
257 * Add a parent onto the end of the parent list.
258 *
259 * @param additionalParent
260 * new parent to add onto the end of the current parent list.
261 */
262 public void addParentId(AnyObjectId additionalParent) {
263 if (parentIds.length == 0) {
264 setParentId(additionalParent);
265 } else {
266 ObjectId[] newParents = new ObjectId[parentIds.length + 1];
267 System.arraycopy(parentIds, 0, newParents, 0, parentIds.length);
268 newParents[parentIds.length] = additionalParent.copy();
269 parentIds = newParents;
270 }
271 }
272
273 /**
274 * Get the complete commit message.
275 *
276 * @return the complete commit message.
277 */
278 public String getMessage() {
279 return message;
280 }
281
282 /**
283 * Set the commit message.
284 *
285 * @param newMessage
286 * the commit message. Should not be null.
287 */
288 public void setMessage(String newMessage) {
289 message = newMessage;
290 }
291
292 /**
293 * Set the encoding for the commit information.
294 *
295 * @param encodingName
296 * the encoding name. See
297 * {@link java.nio.charset.Charset#forName(String)}.
298 * @deprecated use {@link #setEncoding(Charset)} instead.
299 */
300 @Deprecated
301 public void setEncoding(String encodingName) {
302 encoding = Charset.forName(encodingName);
303 }
304
305 /**
306 * Set the encoding for the commit information.
307 *
308 * @param enc
309 * the encoding to use.
310 */
311 public void setEncoding(Charset enc) {
312 encoding = enc;
313 }
314
315 /**
316 * Get the encoding that should be used for the commit message text.
317 *
318 * @return the encoding that should be used for the commit message text.
319 */
320 public Charset getEncoding() {
321 return encoding;
322 }
323
324 /**
325 * Format this builder's state as a commit object.
326 *
327 * @return this object in the canonical commit format, suitable for storage
328 * in a repository.
329 * @throws java.io.UnsupportedEncodingException
330 * the encoding specified by {@link #getEncoding()} is not
331 * supported by this Java runtime.
332 */
333 public byte[] build() throws UnsupportedEncodingException {
334 ByteArrayOutputStream os = new ByteArrayOutputStream();
335 OutputStreamWriter w = new OutputStreamWriter(os, getEncoding());
336 try {
337 os.write(htree);
338 os.write(' ');
339 getTreeId().copyTo(os);
340 os.write('\n');
341
342 for (ObjectId p : getParentIds()) {
343 os.write(hparent);
344 os.write(' ');
345 p.copyTo(os);
346 os.write('\n');
347 }
348
349 os.write(hauthor);
350 os.write(' ');
351 w.write(getAuthor().toExternalString());
352 w.flush();
353 os.write('\n');
354
355 os.write(hcommitter);
356 os.write(' ');
357 w.write(getCommitter().toExternalString());
358 w.flush();
359 os.write('\n');
360
361 if (getGpgSignature() != null) {
362 os.write(hgpgsig);
363 os.write(' ');
364 writeGpgSignatureString(getGpgSignature().toExternalString(), os);
365 os.write('\n');
366 }
367
368 if (getEncoding() != UTF_8) {
369 os.write(hencoding);
370 os.write(' ');
371 os.write(Constants.encodeASCII(getEncoding().name()));
372 os.write('\n');
373 }
374
375 os.write('\n');
376
377 if (getMessage() != null) {
378 w.write(getMessage());
379 w.flush();
380 }
381 } catch (IOException err) {
382 // This should never occur, the only way to get it above is
383 // for the ByteArrayOutputStream to throw, but it doesn't.
384 //
385 throw new RuntimeException(err);
386 }
387 return os.toByteArray();
388 }
389
390 /**
391 * Writes signature to output as per <a href=
392 * "https://github.com/git/git/blob/master/Documentation/technical/signature-format.txt#L66,L89">gpgsig
393 * header</a>.
394 * <p>
395 * CRLF and CR will be sanitized to LF and signature will have a hanging
396 * indent of one space starting with line two.
397 * </p>
398 *
399 * @param in
400 * signature string with line breaks
401 * @param out
402 * output stream
403 * @throws IOException
404 * thrown by the output stream
405 * @throws IllegalArgumentException
406 * if the signature string contains non 7-bit ASCII chars
407 */
408 static void writeGpgSignatureString(String in, OutputStream out)
409 throws IOException, IllegalArgumentException {
410 for (int i = 0; i < in.length(); ++i) {
411 char ch = in.charAt(i);
412 if (ch == '\r') {
413 if (i + 1 < in.length() && in.charAt(i + 1) == '\n') {
414 out.write('\n');
415 out.write(' ');
416 ++i;
417 } else {
418 out.write('\n');
419 out.write(' ');
420 }
421 } else if (ch == '\n') {
422 out.write('\n');
423 out.write(' ');
424 } else {
425 // sanity check
426 if (ch > 127)
427 throw new IllegalArgumentException(MessageFormat
428 .format(JGitText.get().notASCIIString, in));
429 out.write(ch);
430 }
431 }
432 }
433
434 /**
435 * Format this builder's state as a commit object.
436 *
437 * @return this object in the canonical commit format, suitable for storage
438 * in a repository.
439 * @throws java.io.UnsupportedEncodingException
440 * the encoding specified by {@link #getEncoding()} is not
441 * supported by this Java runtime.
442 */
443 public byte[] toByteArray() throws UnsupportedEncodingException {
444 return build();
445 }
446
447 /** {@inheritDoc} */
448 @SuppressWarnings("nls")
449 @Override
450 public String toString() {
451 StringBuilder r = new StringBuilder();
452 r.append("Commit");
453 r.append("={\n");
454
455 r.append("tree ");
456 r.append(treeId != null ? treeId.name() : "NOT_SET");
457 r.append("\n");
458
459 for (ObjectId p : parentIds) {
460 r.append("parent ");
461 r.append(p.name());
462 r.append("\n");
463 }
464
465 r.append("author ");
466 r.append(author != null ? author.toString() : "NOT_SET");
467 r.append("\n");
468
469 r.append("committer ");
470 r.append(committer != null ? committer.toString() : "NOT_SET");
471 r.append("\n");
472
473 r.append("gpgSignature ");
474 r.append(gpgSignature != null ? gpgSignature.toString() : "NOT_SET");
475 r.append("\n");
476
477 if (encoding != null && encoding != UTF_8) {
478 r.append("encoding ");
479 r.append(encoding.name());
480 r.append("\n");
481 }
482
483 r.append("\n");
484 r.append(message != null ? message : "");
485 r.append("}");
486 return r.toString();
487 }
488 }