View Javadoc
1   /*
2    * Copyright (C) 2014, 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  package org.eclipse.jgit.gitrepo;
44  
45  import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME;
46  import static org.eclipse.jgit.lib.Constants.R_REMOTES;
47  
48  import java.io.File;
49  import java.io.FileInputStream;
50  import java.io.IOException;
51  import java.io.InputStream;
52  import java.text.MessageFormat;
53  import java.util.ArrayList;
54  import java.util.List;
55  import java.util.Map;
56  import java.util.Set;
57  
58  import org.eclipse.jgit.annotations.Nullable;
59  import org.eclipse.jgit.api.Git;
60  import org.eclipse.jgit.api.GitCommand;
61  import org.eclipse.jgit.api.SubmoduleAddCommand;
62  import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
63  import org.eclipse.jgit.api.errors.GitAPIException;
64  import org.eclipse.jgit.api.errors.JGitInternalException;
65  import org.eclipse.jgit.dircache.DirCache;
66  import org.eclipse.jgit.dircache.DirCacheBuilder;
67  import org.eclipse.jgit.dircache.DirCacheEntry;
68  import org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader;
69  import org.eclipse.jgit.gitrepo.RepoProject.CopyFile;
70  import org.eclipse.jgit.gitrepo.internal.RepoText;
71  import org.eclipse.jgit.internal.JGitText;
72  import org.eclipse.jgit.lib.CommitBuilder;
73  import org.eclipse.jgit.lib.Config;
74  import org.eclipse.jgit.lib.Constants;
75  import org.eclipse.jgit.lib.FileMode;
76  import org.eclipse.jgit.lib.ObjectId;
77  import org.eclipse.jgit.lib.ObjectInserter;
78  import org.eclipse.jgit.lib.ObjectReader;
79  import org.eclipse.jgit.lib.PersonIdent;
80  import org.eclipse.jgit.lib.ProgressMonitor;
81  import org.eclipse.jgit.lib.Ref;
82  import org.eclipse.jgit.lib.RefDatabase;
83  import org.eclipse.jgit.lib.RefUpdate;
84  import org.eclipse.jgit.lib.RefUpdate.Result;
85  import org.eclipse.jgit.lib.Repository;
86  import org.eclipse.jgit.revwalk.RevCommit;
87  import org.eclipse.jgit.revwalk.RevWalk;
88  import org.eclipse.jgit.util.FileUtils;
89  
90  /**
91   * A class used to execute a repo command.
92   *
93   * This will parse a repo XML manifest, convert it into .gitmodules file and the
94   * repository config file.
95   *
96   * If called against a bare repository, it will replace all the existing content
97   * of the repository with the contents populated from the manifest.
98   *
99   * repo manifest allows projects overlapping, e.g. one project's path is
100  * "foo" and another project's path is "foo/bar". This won't
101  * work in git submodule, so we'll skip all the sub projects
102  * ("foo/bar" in the example) while converting.
103  *
104  * @see <a href="https://code.google.com/p/git-repo/">git-repo project page</a>
105  * @since 3.4
106  */
107 public class RepoCommand extends GitCommand<RevCommit> {
108 	private String path;
109 	private String uri;
110 	private String groupsParam;
111 	private String branch;
112 	private String targetBranch = Constants.HEAD;
113 	private boolean recordRemoteBranch = false;
114 	private boolean recordSubmoduleLabels = false;
115 	private boolean recordShallowSubmodules = false;
116 	private PersonIdent author;
117 	private RemoteReader callback;
118 	private InputStream inputStream;
119 	private IncludedFileReader includedReader;
120 	private boolean ignoreRemoteFailures = false;
121 
122 	private List<RepoProject> bareProjects;
123 	private Git git;
124 	private ProgressMonitor monitor;
125 
126 	/**
127 	 * A callback to get ref sha1 of a repository from its uri.
128 	 *
129 	 * We provided a default implementation {@link DefaultRemoteReader} to
130 	 * use ls-remote command to read the sha1 from the repository and clone the
131 	 * repository to read the file. Callers may have their own quicker
132 	 * implementation.
133 	 *
134 	 * @since 3.4
135 	 */
136 	public interface RemoteReader {
137 		/**
138 		 * Read a remote ref sha1.
139 		 *
140 		 * @param uri
141 		 *            The URI of the remote repository
142 		 * @param ref
143 		 *            The ref (branch/tag/etc.) to read
144 		 * @return the sha1 of the remote repository, or null if the ref does
145 		 *         not exist.
146 		 * @throws GitAPIException
147 		 */
148 		@Nullable
149 		public ObjectId sha1(String uri, String ref) throws GitAPIException;
150 
151 		/**
152 		 * Read a file from a remote repository.
153 		 *
154 		 * @param uri
155 		 *            The URI of the remote repository
156 		 * @param ref
157 		 *            The ref (branch/tag/etc.) to read
158 		 * @param path
159 		 *            The relative path (inside the repo) to the file to read
160 		 * @return the file content.
161 		 * @throws GitAPIException
162 		 * @throws IOException
163 		 * @since 3.5
164 		 */
165 		public byte[] readFile(String uri, String ref, String path)
166 				throws GitAPIException, IOException;
167 	}
168 
169 	/** A default implementation of {@link RemoteReader} callback. */
170 	public static class DefaultRemoteReader implements RemoteReader {
171 		public ObjectId sha1(String uri, String ref) throws GitAPIException {
172 			Map<String, Ref> map = Git
173 					.lsRemoteRepository()
174 					.setRemote(uri)
175 					.callAsMap();
176 			Ref r = RefDatabase.findRef(map, ref);
177 			return r != null ? r.getObjectId() : null;
178 		}
179 
180 		public byte[] readFile(String uri, String ref, String path)
181 				throws GitAPIException, IOException {
182 			File dir = FileUtils.createTempDir("jgit_", ".git", null); //$NON-NLS-1$ //$NON-NLS-2$
183 			Repository repo = Git
184 					.cloneRepository()
185 					.setBare(true)
186 					.setDirectory(dir)
187 					.setURI(uri)
188 					.call()
189 					.getRepository();
190 			try {
191 				return readFileFromRepo(repo, ref, path);
192 			} finally {
193 				repo.close();
194 				FileUtils.delete(dir, FileUtils.RECURSIVE);
195 			}
196 		}
197 
198 		/**
199 		 * Read a file from the repository
200 		 *
201 		 * @param repo
202 		 *            The repository containing the file
203 		 * @param ref
204 		 *            The ref (branch/tag/etc.) to read
205 		 * @param path
206 		 *            The relative path (inside the repo) to the file to read
207 		 * @return the file's content
208 		 * @throws GitAPIException
209 		 * @throws IOException
210 		 * @since 3.5
211 		 */
212 		protected byte[] readFileFromRepo(Repository repo,
213 				String ref, String path) throws GitAPIException, IOException {
214 			try (ObjectReader reader = repo.newObjectReader()) {
215 				ObjectId oid = repo.resolve(ref + ":" + path); //$NON-NLS-1$
216 				return reader.open(oid).getBytes(Integer.MAX_VALUE);
217 			}
218 		}
219 	}
220 
221 	@SuppressWarnings("serial")
222 	private static class ManifestErrorException extends GitAPIException {
223 		ManifestErrorException(Throwable cause) {
224 			super(RepoText.get().invalidManifest, cause);
225 		}
226 	}
227 
228 	@SuppressWarnings("serial")
229 	private static class RemoteUnavailableException extends GitAPIException {
230 		RemoteUnavailableException(String uri) {
231 			super(MessageFormat.format(RepoText.get().errorRemoteUnavailable, uri));
232 		}
233 	}
234 
235 	/**
236 	 * @param repo
237 	 */
238 	public RepoCommand(Repository repo) {
239 		super(repo);
240 	}
241 
242 	/**
243 	 * Set path to the manifest XML file.
244 	 * <p>
245 	 * Calling {@link #setInputStream} will ignore the path set here.
246 	 *
247 	 * @param path
248 	 *            (with <code>/</code> as separator)
249 	 * @return this command
250 	 */
251 	public RepoCommand setPath(String path) {
252 		this.path = path;
253 		return this;
254 	}
255 
256 	/**
257 	 * Set the input stream to the manifest XML.
258 	 * <p>
259 	 * Setting inputStream will ignore the path set. It will be closed in
260 	 * {@link #call}.
261 	 *
262 	 * @param inputStream
263 	 * @return this command
264 	 * @since 3.5
265 	 */
266 	public RepoCommand setInputStream(InputStream inputStream) {
267 		this.inputStream = inputStream;
268 		return this;
269 	}
270 
271 	/**
272 	 * Set base URI of the pathes inside the XML
273 	 *
274 	 * @param uri
275 	 * @return this command
276 	 */
277 	public RepoCommand setURI(String uri) {
278 		this.uri = uri;
279 		return this;
280 	}
281 
282 	/**
283 	 * Set groups to sync
284 	 *
285 	 * @param groups groups separated by comma, examples: default|all|G1,-G2,-G3
286 	 * @return this command
287 	 */
288 	public RepoCommand setGroups(String groups) {
289 		this.groupsParam = groups;
290 		return this;
291 	}
292 
293 	/**
294 	 * Set default branch.
295 	 * <p>
296 	 * This is generally the name of the branch the manifest file was in. If
297 	 * there's no default revision (branch) specified in manifest and no
298 	 * revision specified in project, this branch will be used.
299 	 *
300 	 * @param branch
301 	 * @return this command
302 	 */
303 	public RepoCommand setBranch(String branch) {
304 		this.branch = branch;
305 		return this;
306 	}
307 
308 	/**
309 	 * Set target branch.
310 	 * <p>
311 	 * This is the target branch of the super project to be updated. If not set,
312 	 * default is HEAD.
313 	 * <p>
314 	 * For non-bare repositories, HEAD will always be used and this will be
315 	 * ignored.
316 	 *
317 	 * @param branch
318 	 * @return this command
319 	 * @since 4.1
320 	 */
321 	public RepoCommand setTargetBranch(String branch) {
322 		this.targetBranch = Constants.R_HEADS + branch;
323 		return this;
324 	}
325 
326 	/**
327 	 * Set whether the branch name should be recorded in .gitmodules.
328 	 * <p>
329 	 * Submodule entries in .gitmodules can include a "branch" field
330 	 * to indicate what remote branch each submodule tracks.
331 	 * <p>
332 	 * That field is used by "git submodule update --remote" to update
333 	 * to the tip of the tracked branch when asked and by Gerrit to
334 	 * update the superproject when a change on that branch is merged.
335 	 * <p>
336 	 * Subprojects that request a specific commit or tag will not have
337 	 * a branch name recorded.
338 	 * <p>
339 	 * Not implemented for non-bare repositories.
340 	 *
341 	 * @param enable Whether to record the branch name
342 	 * @return this command
343 	 * @since 4.2
344 	 */
345 	public RepoCommand setRecordRemoteBranch(boolean enable) {
346 		this.recordRemoteBranch = enable;
347 		return this;
348 	}
349 
350 	/**
351 	 * Set whether the labels field should be recorded as a label in
352 	 * .gitattributes.
353 	 * <p>
354 	 * Not implemented for non-bare repositories.
355 	 *
356 	 * @param enable Whether to record the labels in the .gitattributes
357 	 * @return this command
358 	 * @since 4.4
359 	 */
360 	public RepoCommand setRecordSubmoduleLabels(boolean enable) {
361 		this.recordSubmoduleLabels = enable;
362 		return this;
363 	}
364 
365 	/**
366 	 * Set whether the clone-depth field should be recorded as a shallow
367 	 * recommendation in .gitmodules.
368 	 * <p>
369 	 * Not implemented for non-bare repositories.
370 	 *
371 	 * @param enable Whether to record the shallow recommendation.
372 	 * @return this command
373 	 * @since 4.4
374 	 */
375 	public RepoCommand setRecommendShallow(boolean enable) {
376 		this.recordShallowSubmodules = enable;
377 		return this;
378 	}
379 
380 	/**
381 	 * The progress monitor associated with the clone operation. By default,
382 	 * this is set to <code>NullProgressMonitor</code>
383 	 *
384 	 * @see org.eclipse.jgit.lib.NullProgressMonitor
385 	 * @param monitor
386 	 * @return this command
387 	 */
388 	public RepoCommand setProgressMonitor(final ProgressMonitor monitor) {
389 		this.monitor = monitor;
390 		return this;
391 	}
392 
393 	/**
394 	 * Set whether to skip projects whose commits don't exist remotely.
395 	 * <p>
396 	 * When set to true, we'll just skip the manifest entry and continue
397 	 * on to the next one.
398 	 * <p>
399 	 * When set to false (default), we'll throw an error when remote
400 	 * failures occur.
401 	 * <p>
402 	 * Not implemented for non-bare repositories.
403 	 *
404 	 * @param ignore Whether to ignore the remote failures.
405 	 * @return this command
406 	 * @since 4.3
407 	 */
408 	public RepoCommand setIgnoreRemoteFailures(boolean ignore) {
409 		this.ignoreRemoteFailures = ignore;
410 		return this;
411 	}
412 
413 	/**
414 	 * Set the author/committer for the bare repository commit.
415 	 * <p>
416 	 * For non-bare repositories, the current user will be used and this will be
417 	 * ignored.
418 	 *
419 	 * @param author
420 	 * @return this command
421 	 */
422 	public RepoCommand setAuthor(final PersonIdent author) {
423 		this.author = author;
424 		return this;
425 	}
426 
427 	/**
428 	 * Set the GetHeadFromUri callback.
429 	 *
430 	 * This is only used in bare repositories.
431 	 *
432 	 * @param callback
433 	 * @return this command
434 	 */
435 	public RepoCommand setRemoteReader(final RemoteReader callback) {
436 		this.callback = callback;
437 		return this;
438 	}
439 
440 	/**
441 	 * Set the IncludedFileReader callback.
442 	 *
443 	 * @param reader
444 	 * @return this command
445 	 * @since 4.0
446 	 */
447 	public RepoCommand setIncludedFileReader(IncludedFileReader reader) {
448 		this.includedReader = reader;
449 		return this;
450 	}
451 
452 	@Override
453 	public RevCommit call() throws GitAPIException {
454 		try {
455 			checkCallable();
456 			if (uri == null || uri.length() == 0)
457 				throw new IllegalArgumentException(
458 						JGitText.get().uriNotConfigured);
459 			if (inputStream == null) {
460 				if (path == null || path.length() == 0)
461 					throw new IllegalArgumentException(
462 							JGitText.get().pathNotConfigured);
463 				try {
464 					inputStream = new FileInputStream(path);
465 				} catch (IOException e) {
466 					throw new IllegalArgumentException(
467 							JGitText.get().pathNotConfigured);
468 				}
469 			}
470 
471 			if (repo.isBare()) {
472 				bareProjects = new ArrayList<RepoProject>();
473 				if (author == null)
474 					author = new PersonIdent(repo);
475 				if (callback == null)
476 					callback = new DefaultRemoteReader();
477 			} else
478 				git = new Git(repo);
479 
480 			ManifestParser parser = new ManifestParser(
481 					includedReader, path, branch, uri, groupsParam, repo);
482 			try {
483 				parser.read(inputStream);
484 				for (RepoProject proj : parser.getFilteredProjects()) {
485 					addSubmodule(proj.getUrl(),
486 							proj.getPath(),
487 							proj.getRevision(),
488 							proj.getCopyFiles(),
489 							proj.getGroups(),
490 							proj.getRecommendShallow());
491 				}
492 			} catch (GitAPIException | IOException e) {
493 				throw new ManifestErrorException(e);
494 			}
495 		} finally {
496 			try {
497 				if (inputStream != null)
498 					inputStream.close();
499 			} catch (IOException e) {
500 				// Just ignore it, it's not important.
501 			}
502 		}
503 
504 		if (repo.isBare()) {
505 			DirCache index = DirCache.newInCore();
506 			DirCacheBuilder builder = index.builder();
507 			ObjectInserter inserter = repo.newObjectInserter();
508 			try (RevWalk rw = new RevWalk(repo)) {
509 				Config cfg = new Config();
510 				StringBuilder attributes = new StringBuilder();
511 				for (RepoProject proj : bareProjects) {
512 					String name = proj.getPath();
513 					String nameUri = proj.getName();
514 					ObjectId objectId;
515 					if (ObjectId.isId(proj.getRevision())
516 							&& !ignoreRemoteFailures) {
517 						objectId = ObjectId.fromString(proj.getRevision());
518 					} else {
519 						objectId = callback.sha1(nameUri, proj.getRevision());
520 						if (objectId == null) {
521 							if (ignoreRemoteFailures) {
522 								continue;
523 							}
524 							throw new RemoteUnavailableException(nameUri);
525 						}
526 						if (recordRemoteBranch) {
527 							// can be branch or tag
528 							cfg.setString("submodule", name, "branch", //$NON-NLS-1$ //$NON-NLS-2$
529 									proj.getRevision());
530 						}
531 
532 						if (recordShallowSubmodules && proj.getRecommendShallow() != null) {
533 							// The shallow recommendation is losing information.
534 							// As the repo manifests stores the recommended
535 							// depth in the 'clone-depth' field, while
536 							// git core only uses a binary 'shallow = true/false'
537 							// hint, we'll map any depth to 'shallow = true'
538 							cfg.setBoolean("submodule", name, "shallow", //$NON-NLS-1$ //$NON-NLS-2$
539 									true);
540 						}
541 					}
542 					if (recordSubmoduleLabels) {
543 						StringBuilder rec = new StringBuilder();
544 						rec.append("/"); //$NON-NLS-1$
545 						rec.append(name);
546 						for (String group : proj.getGroups()) {
547 							rec.append(" "); //$NON-NLS-1$
548 							rec.append(group);
549 						}
550 						rec.append("\n"); //$NON-NLS-1$
551 						attributes.append(rec.toString());
552 					}
553 					cfg.setString("submodule", name, "path", name); //$NON-NLS-1$ //$NON-NLS-2$
554 					cfg.setString("submodule", name, "url", nameUri); //$NON-NLS-1$ //$NON-NLS-2$
555 
556 					// create gitlink
557 					DirCacheEntry dcEntry = new DirCacheEntry(name);
558 					dcEntry.setObjectId(objectId);
559 					dcEntry.setFileMode(FileMode.GITLINK);
560 					builder.add(dcEntry);
561 
562 					for (CopyFile copyfile : proj.getCopyFiles()) {
563 						byte[] src = callback.readFile(
564 								nameUri, proj.getRevision(), copyfile.src);
565 						objectId = inserter.insert(Constants.OBJ_BLOB, src);
566 						dcEntry = new DirCacheEntry(copyfile.dest);
567 						dcEntry.setObjectId(objectId);
568 						dcEntry.setFileMode(FileMode.REGULAR_FILE);
569 						builder.add(dcEntry);
570 					}
571 				}
572 				String content = cfg.toText();
573 
574 				// create a new DirCacheEntry for .gitmodules file.
575 				final DirCacheEntry dcEntry = new DirCacheEntry(Constants.DOT_GIT_MODULES);
576 				ObjectId objectId = inserter.insert(Constants.OBJ_BLOB,
577 						content.getBytes(Constants.CHARACTER_ENCODING));
578 				dcEntry.setObjectId(objectId);
579 				dcEntry.setFileMode(FileMode.REGULAR_FILE);
580 				builder.add(dcEntry);
581 
582 				if (recordSubmoduleLabels) {
583 					// create a new DirCacheEntry for .gitattributes file.
584 					final DirCacheEntry dcEntryAttr = new DirCacheEntry(Constants.DOT_GIT_ATTRIBUTES);
585 					ObjectId attrId = inserter.insert(Constants.OBJ_BLOB,
586 							attributes.toString().getBytes(Constants.CHARACTER_ENCODING));
587 					dcEntryAttr.setObjectId(attrId);
588 					dcEntryAttr.setFileMode(FileMode.REGULAR_FILE);
589 					builder.add(dcEntryAttr);
590 				}
591 
592 				builder.finish();
593 				ObjectId treeId = index.writeTree(inserter);
594 
595 				// Create a Commit object, populate it and write it
596 				ObjectId headId = repo.resolve(targetBranch + "^{commit}"); //$NON-NLS-1$
597 				CommitBuilder commit = new CommitBuilder();
598 				commit.setTreeId(treeId);
599 				if (headId != null)
600 					commit.setParentIds(headId);
601 				commit.setAuthor(author);
602 				commit.setCommitter(author);
603 				commit.setMessage(RepoText.get().repoCommitMessage);
604 
605 				ObjectId commitId = inserter.insert(commit);
606 				inserter.flush();
607 
608 				RefUpdate ru = repo.updateRef(targetBranch);
609 				ru.setNewObjectId(commitId);
610 				ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId());
611 				Result rc = ru.update(rw);
612 
613 				switch (rc) {
614 					case NEW:
615 					case FORCED:
616 					case FAST_FORWARD:
617 						// Successful. Do nothing.
618 						break;
619 					case REJECTED:
620 					case LOCK_FAILURE:
621 						throw new ConcurrentRefUpdateException(
622 								MessageFormat.format(
623 										JGitText.get().cannotLock, targetBranch),
624 								ru.getRef(),
625 								rc);
626 					default:
627 						throw new JGitInternalException(MessageFormat.format(
628 								JGitText.get().updatingRefFailed,
629 								targetBranch, commitId.name(), rc));
630 				}
631 
632 				return rw.parseCommit(commitId);
633 			} catch (IOException e) {
634 				throw new ManifestErrorException(e);
635 			}
636 		} else {
637 			return git
638 				.commit()
639 				.setMessage(RepoText.get().repoCommitMessage)
640 				.call();
641 		}
642 	}
643 
644 	private void addSubmodule(String url, String name, String revision,
645 			List<CopyFile> copyfiles, Set<String> groups, String recommendShallow)
646 			throws GitAPIException, IOException {
647 		if (repo.isBare()) {
648 			RepoProject proj = new RepoProject(url, name, revision, null, groups, recommendShallow);
649 			proj.addCopyFiles(copyfiles);
650 			bareProjects.add(proj);
651 		} else {
652 			SubmoduleAddCommand add = git
653 				.submoduleAdd()
654 				.setPath(name)
655 				.setURI(url);
656 			if (monitor != null)
657 				add.setProgressMonitor(monitor);
658 
659 			Repository subRepo = add.call();
660 			if (revision != null) {
661 				try (Git sub = new Git(subRepo)) {
662 					sub.checkout().setName(findRef(revision, subRepo))
663 							.call();
664 				}
665 				subRepo.close();
666 				git.add().addFilepattern(name).call();
667 			}
668 			for (CopyFile copyfile : copyfiles) {
669 				copyfile.copy();
670 				git.add().addFilepattern(copyfile.dest).call();
671 			}
672 		}
673 	}
674 
675 	private static String findRef(String ref, Repository repo)
676 			throws IOException {
677 		if (!ObjectId.isId(ref)) {
678 			Ref r = repo.exactRef(R_REMOTES + DEFAULT_REMOTE_NAME + "/" + ref); //$NON-NLS-1$
679 			if (r != null)
680 				return r.getName();
681 		}
682 		return ref;
683 	}
684 }