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 java.nio.charset.StandardCharsets.UTF_8;
46  import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME;
47  import static org.eclipse.jgit.lib.Constants.R_REMOTES;
48  
49  import java.io.File;
50  import java.io.FileInputStream;
51  import java.io.IOException;
52  import java.io.InputStream;
53  import java.net.URI;
54  import java.text.MessageFormat;
55  import java.util.ArrayList;
56  import java.util.List;
57  import java.util.Map;
58  import java.util.Objects;
59  import java.util.StringJoiner;
60  import java.util.TreeMap;
61  
62  import org.eclipse.jgit.annotations.Nullable;
63  import org.eclipse.jgit.api.Git;
64  import org.eclipse.jgit.api.GitCommand;
65  import org.eclipse.jgit.api.SubmoduleAddCommand;
66  import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
67  import org.eclipse.jgit.api.errors.GitAPIException;
68  import org.eclipse.jgit.api.errors.JGitInternalException;
69  import org.eclipse.jgit.dircache.DirCache;
70  import org.eclipse.jgit.dircache.DirCacheBuilder;
71  import org.eclipse.jgit.dircache.DirCacheEntry;
72  import org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader;
73  import org.eclipse.jgit.gitrepo.RepoProject.CopyFile;
74  import org.eclipse.jgit.gitrepo.RepoProject.LinkFile;
75  import org.eclipse.jgit.gitrepo.internal.RepoText;
76  import org.eclipse.jgit.internal.JGitText;
77  import org.eclipse.jgit.lib.CommitBuilder;
78  import org.eclipse.jgit.lib.Config;
79  import org.eclipse.jgit.lib.Constants;
80  import org.eclipse.jgit.lib.FileMode;
81  import org.eclipse.jgit.lib.ObjectId;
82  import org.eclipse.jgit.lib.ObjectInserter;
83  import org.eclipse.jgit.lib.ObjectReader;
84  import org.eclipse.jgit.lib.PersonIdent;
85  import org.eclipse.jgit.lib.ProgressMonitor;
86  import org.eclipse.jgit.lib.Ref;
87  import org.eclipse.jgit.lib.RefDatabase;
88  import org.eclipse.jgit.lib.RefUpdate;
89  import org.eclipse.jgit.lib.RefUpdate.Result;
90  import org.eclipse.jgit.lib.Repository;
91  import org.eclipse.jgit.revwalk.RevCommit;
92  import org.eclipse.jgit.revwalk.RevWalk;
93  import org.eclipse.jgit.util.FileUtils;
94  
95  /**
96   * A class used to execute a repo command.
97   *
98   * This will parse a repo XML manifest, convert it into .gitmodules file and the
99   * repository config file.
100  *
101  * If called against a bare repository, it will replace all the existing content
102  * of the repository with the contents populated from the manifest.
103  *
104  * repo manifest allows projects overlapping, e.g. one project's manifestPath is
105  * "foo" and another project's manifestPath is "foo/bar". This won't
106  * work in git submodule, so we'll skip all the sub projects
107  * ("foo/bar" in the example) while converting.
108  *
109  * @see <a href="https://code.google.com/p/git-repo/">git-repo project page</a>
110  * @since 3.4
111  */
112 public class RepoCommand extends GitCommand<RevCommit> {
113 	private String manifestPath;
114 	private String baseUri;
115 	private URI targetUri;
116 	private String groupsParam;
117 	private String branch;
118 	private String targetBranch = Constants.HEAD;
119 	private boolean recordRemoteBranch = true;
120 	private boolean recordSubmoduleLabels = true;
121 	private boolean recordShallowSubmodules = true;
122 	private PersonIdent author;
123 	private RemoteReader callback;
124 	private InputStream inputStream;
125 	private IncludedFileReader includedReader;
126 	private boolean ignoreRemoteFailures = false;
127 
128 	private ProgressMonitor monitor;
129 
130 	/**
131 	 * A callback to get ref sha1 of a repository from its uri.
132 	 *
133 	 * We provided a default implementation {@link DefaultRemoteReader} to
134 	 * use ls-remote command to read the sha1 from the repository and clone the
135 	 * repository to read the file. Callers may have their own quicker
136 	 * implementation.
137 	 *
138 	 * @since 3.4
139 	 */
140 	public interface RemoteReader {
141 		/**
142 		 * Read a remote ref sha1.
143 		 *
144 		 * @param uri
145 		 *            The URI of the remote repository
146 		 * @param ref
147 		 *            The ref (branch/tag/etc.) to read
148 		 * @return the sha1 of the remote repository, or null if the ref does
149 		 *         not exist.
150 		 * @throws GitAPIException
151 		 */
152 		@Nullable
153 		public ObjectId sha1(String uri, String ref) throws GitAPIException;
154 
155 		/**
156 		 * Read a file from a remote repository.
157 		 *
158 		 * @param uri
159 		 *            The URI of the remote repository
160 		 * @param ref
161 		 *            The ref (branch/tag/etc.) to read
162 		 * @param path
163 		 *            The relative path (inside the repo) to the file to read
164 		 * @return the file content.
165 		 * @throws GitAPIException
166 		 * @throws IOException
167 		 * @since 3.5
168 		 */
169 		public byte[] readFile(String uri, String ref, String path)
170 				throws GitAPIException, IOException;
171 	}
172 
173 	/** A default implementation of {@link RemoteReader} callback. */
174 	public static class DefaultRemoteReader implements RemoteReader {
175 		@Override
176 		public ObjectId sha1(String uri, String ref) throws GitAPIException {
177 			Map<String, Ref> map = Git
178 					.lsRemoteRepository()
179 					.setRemote(uri)
180 					.callAsMap();
181 			Ref r = RefDatabase.findRef(map, ref);
182 			return r != null ? r.getObjectId() : null;
183 		}
184 
185 		@Override
186 		public byte[] readFile(String uri, String ref, String path)
187 				throws GitAPIException, IOException {
188 			File dir = FileUtils.createTempDir("jgit_", ".git", null); //$NON-NLS-1$ //$NON-NLS-2$
189 			try (Git git = Git.cloneRepository().setBare(true).setDirectory(dir)
190 					.setURI(uri).call()) {
191 				return readFileFromRepo(git.getRepository(), ref, path);
192 			} finally {
193 				FileUtils.delete(dir, FileUtils.RECURSIVE);
194 			}
195 		}
196 
197 		/**
198 		 * Read a file from the repository
199 		 *
200 		 * @param repo
201 		 *            The repository containing the file
202 		 * @param ref
203 		 *            The ref (branch/tag/etc.) to read
204 		 * @param path
205 		 *            The relative path (inside the repo) to the file to read
206 		 * @return the file's content
207 		 * @throws GitAPIException
208 		 * @throws IOException
209 		 * @since 3.5
210 		 */
211 		protected byte[] readFileFromRepo(Repository repo,
212 				String ref, String path) throws GitAPIException, IOException {
213 			try (ObjectReader reader = repo.newObjectReader()) {
214 				ObjectId oid = repo.resolve(ref + ":" + path); //$NON-NLS-1$
215 				return reader.open(oid).getBytes(Integer.MAX_VALUE);
216 			}
217 		}
218 	}
219 
220 	@SuppressWarnings("serial")
221 	private static class ManifestErrorException extends GitAPIException {
222 		ManifestErrorException(Throwable cause) {
223 			super(RepoText.get().invalidManifest, cause);
224 		}
225 	}
226 
227 	@SuppressWarnings("serial")
228 	private static class RemoteUnavailableException extends GitAPIException {
229 		RemoteUnavailableException(String uri) {
230 			super(MessageFormat.format(RepoText.get().errorRemoteUnavailable, uri));
231 		}
232 	}
233 
234 	/**
235 	 * Constructor for RepoCommand
236 	 *
237 	 * @param repo
238 	 *            the {@link org.eclipse.jgit.lib.Repository}
239 	 */
240 	public RepoCommand(Repository repo) {
241 		super(repo);
242 	}
243 
244 	/**
245 	 * Set path to the manifest XML file.
246 	 * <p>
247 	 * Calling {@link #setInputStream} will ignore the path set here.
248 	 *
249 	 * @param path
250 	 *            (with <code>/</code> as separator)
251 	 * @return this command
252 	 */
253 	public RepoCommand setPath(String path) {
254 		this.manifestPath = path;
255 		return this;
256 	}
257 
258 	/**
259 	 * Set the input stream to the manifest XML.
260 	 * <p>
261 	 * Setting inputStream will ignore the path set. It will be closed in
262 	 * {@link #call}.
263 	 *
264 	 * @param inputStream a {@link java.io.InputStream} object.
265 	 * @return this command
266 	 * @since 3.5
267 	 */
268 	public RepoCommand setInputStream(InputStream inputStream) {
269 		this.inputStream = inputStream;
270 		return this;
271 	}
272 
273 	/**
274 	 * Set base URI of the paths inside the XML. This is typically the name of
275 	 * the directory holding the manifest repository, eg. for
276 	 * https://android.googlesource.com/platform/manifest, this should be
277 	 * /platform (if you would run this on android.googlesource.com) or
278 	 * https://android.googlesource.com/platform elsewhere.
279 	 *
280 	 * @param uri
281 	 *            the base URI
282 	 * @return this command
283 	 */
284 	public RepoCommand setURI(String uri) {
285 		this.baseUri = uri;
286 		return this;
287 	}
288 
289 	/**
290 	 * Set the URI of the superproject (this repository), so the .gitmodules
291 	 * file can specify the submodule URLs relative to the superproject.
292 	 *
293 	 * @param uri
294 	 *            the URI of the repository holding the superproject.
295 	 * @return this command
296 	 * @since 4.8
297 	 */
298 	public RepoCommand setTargetURI(String uri) {
299 		// The repo name is interpreted as a directory, for example
300 		// Gerrit (http://gerrit.googlesource.com/gerrit) has a
301 		// .gitmodules referencing ../plugins/hooks, which is
302 		// on http://gerrit.googlesource.com/plugins/hooks,
303 		this.targetUri = URI.create(uri + "/"); //$NON-NLS-1$
304 		return this;
305 	}
306 
307 	/**
308 	 * Set groups to sync
309 	 *
310 	 * @param groups groups separated by comma, examples: default|all|G1,-G2,-G3
311 	 * @return this command
312 	 */
313 	public RepoCommand setGroups(String groups) {
314 		this.groupsParam = groups;
315 		return this;
316 	}
317 
318 	/**
319 	 * Set default branch.
320 	 * <p>
321 	 * This is generally the name of the branch the manifest file was in. If
322 	 * there's no default revision (branch) specified in manifest and no
323 	 * revision specified in project, this branch will be used.
324 	 *
325 	 * @param branch
326 	 *            a branch name
327 	 * @return this command
328 	 */
329 	public RepoCommand setBranch(String branch) {
330 		this.branch = branch;
331 		return this;
332 	}
333 
334 	/**
335 	 * Set target branch.
336 	 * <p>
337 	 * This is the target branch of the super project to be updated. If not set,
338 	 * default is HEAD.
339 	 * <p>
340 	 * For non-bare repositories, HEAD will always be used and this will be
341 	 * ignored.
342 	 *
343 	 * @param branch
344 	 *            branch name
345 	 * @return this command
346 	 * @since 4.1
347 	 */
348 	public RepoCommand setTargetBranch(String branch) {
349 		this.targetBranch = Constants.R_HEADS + branch;
350 		return this;
351 	}
352 
353 	/**
354 	 * Set whether the branch name should be recorded in .gitmodules.
355 	 * <p>
356 	 * Submodule entries in .gitmodules can include a "branch" field
357 	 * to indicate what remote branch each submodule tracks.
358 	 * <p>
359 	 * That field is used by "git submodule update --remote" to update
360 	 * to the tip of the tracked branch when asked and by Gerrit to
361 	 * update the superproject when a change on that branch is merged.
362 	 * <p>
363 	 * Subprojects that request a specific commit or tag will not have
364 	 * a branch name recorded.
365 	 * <p>
366 	 * Not implemented for non-bare repositories.
367 	 *
368 	 * @param enable Whether to record the branch name
369 	 * @return this command
370 	 * @since 4.2
371 	 */
372 	public RepoCommand setRecordRemoteBranch(boolean enable) {
373 		this.recordRemoteBranch = enable;
374 		return this;
375 	}
376 
377 	/**
378 	 * Set whether the labels field should be recorded as a label in
379 	 * .gitattributes.
380 	 * <p>
381 	 * Not implemented for non-bare repositories.
382 	 *
383 	 * @param enable Whether to record the labels in the .gitattributes
384 	 * @return this command
385 	 * @since 4.4
386 	 */
387 	public RepoCommand setRecordSubmoduleLabels(boolean enable) {
388 		this.recordSubmoduleLabels = enable;
389 		return this;
390 	}
391 
392 	/**
393 	 * Set whether the clone-depth field should be recorded as a shallow
394 	 * recommendation in .gitmodules.
395 	 * <p>
396 	 * Not implemented for non-bare repositories.
397 	 *
398 	 * @param enable Whether to record the shallow recommendation.
399 	 * @return this command
400 	 * @since 4.4
401 	 */
402 	public RepoCommand setRecommendShallow(boolean enable) {
403 		this.recordShallowSubmodules = enable;
404 		return this;
405 	}
406 
407 	/**
408 	 * The progress monitor associated with the clone operation. By default,
409 	 * this is set to <code>NullProgressMonitor</code>
410 	 *
411 	 * @see org.eclipse.jgit.lib.NullProgressMonitor
412 	 * @param monitor
413 	 *            a {@link org.eclipse.jgit.lib.ProgressMonitor}
414 	 * @return this command
415 	 */
416 	public RepoCommand setProgressMonitor(ProgressMonitor monitor) {
417 		this.monitor = monitor;
418 		return this;
419 	}
420 
421 	/**
422 	 * Set whether to skip projects whose commits don't exist remotely.
423 	 * <p>
424 	 * When set to true, we'll just skip the manifest entry and continue
425 	 * on to the next one.
426 	 * <p>
427 	 * When set to false (default), we'll throw an error when remote
428 	 * failures occur.
429 	 * <p>
430 	 * Not implemented for non-bare repositories.
431 	 *
432 	 * @param ignore Whether to ignore the remote failures.
433 	 * @return this command
434 	 * @since 4.3
435 	 */
436 	public RepoCommand setIgnoreRemoteFailures(boolean ignore) {
437 		this.ignoreRemoteFailures = ignore;
438 		return this;
439 	}
440 
441 	/**
442 	 * Set the author/committer for the bare repository commit.
443 	 * <p>
444 	 * For non-bare repositories, the current user will be used and this will be
445 	 * ignored.
446 	 *
447 	 * @param author
448 	 *            the author's {@link org.eclipse.jgit.lib.PersonIdent}
449 	 * @return this command
450 	 */
451 	public RepoCommand setAuthor(PersonIdent author) {
452 		this.author = author;
453 		return this;
454 	}
455 
456 	/**
457 	 * Set the GetHeadFromUri callback.
458 	 *
459 	 * This is only used in bare repositories.
460 	 *
461 	 * @param callback
462 	 *            a {@link org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader}
463 	 *            object.
464 	 * @return this command
465 	 */
466 	public RepoCommand setRemoteReader(RemoteReader callback) {
467 		this.callback = callback;
468 		return this;
469 	}
470 
471 	/**
472 	 * Set the IncludedFileReader callback.
473 	 *
474 	 * @param reader
475 	 *            a
476 	 *            {@link org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader}
477 	 *            object.
478 	 * @return this command
479 	 * @since 4.0
480 	 */
481 	public RepoCommand setIncludedFileReader(IncludedFileReader reader) {
482 		this.includedReader = reader;
483 		return this;
484 	}
485 
486 	/** {@inheritDoc} */
487 	@Override
488 	public RevCommit call() throws GitAPIException {
489 		checkCallable();
490 		if (baseUri == null) {
491 			baseUri = ""; //$NON-NLS-1$
492 		}
493 		if (inputStream == null) {
494 			if (manifestPath == null || manifestPath.length() == 0)
495 				throw new IllegalArgumentException(
496 						JGitText.get().pathNotConfigured);
497 			try {
498 				inputStream = new FileInputStream(manifestPath);
499 			} catch (IOException e) {
500 				throw new IllegalArgumentException(
501 						JGitText.get().pathNotConfigured);
502 			}
503 		}
504 
505 		List<RepoProject> filteredProjects;
506 		try {
507 			ManifestParser parser = new ManifestParser(includedReader,
508 					manifestPath, branch, baseUri, groupsParam, repo);
509 			parser.read(inputStream);
510 			filteredProjects = parser.getFilteredProjects();
511 		} catch (IOException e) {
512 			throw new ManifestErrorException(e);
513 		} finally {
514 			try {
515 				inputStream.close();
516 			} catch (IOException e) {
517 				// Just ignore it, it's not important.
518 			}
519 		}
520 
521 		if (repo.isBare()) {
522 			if (author == null)
523 				author = new PersonIdent(repo);
524 			if (callback == null)
525 				callback = new DefaultRemoteReader();
526 			List<RepoProject> renamedProjects = renameProjects(filteredProjects);
527 
528 			DirCache index = DirCache.newInCore();
529 			DirCacheBuilder builder = index.builder();
530 			ObjectInserter inserter = repo.newObjectInserter();
531 			try (RevWalk rw = new RevWalk(repo)) {
532 				Config cfg = new Config();
533 				StringBuilder attributes = new StringBuilder();
534 				for (RepoProject proj : renamedProjects) {
535 					String name = proj.getName();
536 					String path = proj.getPath();
537 					String url = proj.getUrl();
538 					ObjectId objectId;
539 					if (ObjectId.isId(proj.getRevision())) {
540 						objectId = ObjectId.fromString(proj.getRevision());
541 					} else {
542 						objectId = callback.sha1(url, proj.getRevision());
543 						if (objectId == null && !ignoreRemoteFailures) {
544 							throw new RemoteUnavailableException(url);
545 						}
546 						if (recordRemoteBranch) {
547 							// can be branch or tag
548 							cfg.setString("submodule", name, "branch", //$NON-NLS-1$ //$NON-NLS-2$
549 									proj.getRevision());
550 						}
551 
552 						if (recordShallowSubmodules && proj.getRecommendShallow() != null) {
553 							// The shallow recommendation is losing information.
554 							// As the repo manifests stores the recommended
555 							// depth in the 'clone-depth' field, while
556 							// git core only uses a binary 'shallow = true/false'
557 							// hint, we'll map any depth to 'shallow = true'
558 							cfg.setBoolean("submodule", name, "shallow", //$NON-NLS-1$ //$NON-NLS-2$
559 									true);
560 						}
561 					}
562 					if (recordSubmoduleLabels) {
563 						StringBuilder rec = new StringBuilder();
564 						rec.append("/"); //$NON-NLS-1$
565 						rec.append(path);
566 						for (String group : proj.getGroups()) {
567 							rec.append(" "); //$NON-NLS-1$
568 							rec.append(group);
569 						}
570 						rec.append("\n"); //$NON-NLS-1$
571 						attributes.append(rec.toString());
572 					}
573 
574 					URI submodUrl = URI.create(url);
575 					if (targetUri != null) {
576 						submodUrl = relativize(targetUri, submodUrl);
577 					}
578 					cfg.setString("submodule", name, "path", path); //$NON-NLS-1$ //$NON-NLS-2$
579 					cfg.setString("submodule", name, "url", //$NON-NLS-1$ //$NON-NLS-2$
580 							submodUrl.toString());
581 
582 					// create gitlink
583 					if (objectId != null) {
584 						DirCacheEntry dcEntry = new DirCacheEntry(path);
585 						dcEntry.setObjectId(objectId);
586 						dcEntry.setFileMode(FileMode.GITLINK);
587 						builder.add(dcEntry);
588 
589 						for (CopyFile copyfile : proj.getCopyFiles()) {
590 							byte[] src = callback.readFile(
591 								url, proj.getRevision(), copyfile.src);
592 							objectId = inserter.insert(Constants.OBJ_BLOB, src);
593 							dcEntry = new DirCacheEntry(copyfile.dest);
594 							dcEntry.setObjectId(objectId);
595 							dcEntry.setFileMode(FileMode.REGULAR_FILE);
596 							builder.add(dcEntry);
597 						}
598 						for (LinkFile linkfile : proj.getLinkFiles()) {
599 							String link;
600 							if (linkfile.dest.contains("/")) { //$NON-NLS-1$
601 								link = FileUtils.relativizeGitPath(
602 									linkfile.dest.substring(0,
603 										linkfile.dest.lastIndexOf('/')),
604 									proj.getPath() + "/" + linkfile.src); //$NON-NLS-1$
605 							} else {
606 								link = proj.getPath() + "/" + linkfile.src; //$NON-NLS-1$
607 							}
608 
609 							objectId = inserter.insert(Constants.OBJ_BLOB,
610 									link.getBytes(UTF_8));
611 							dcEntry = new DirCacheEntry(linkfile.dest);
612 							dcEntry.setObjectId(objectId);
613 							dcEntry.setFileMode(FileMode.SYMLINK);
614 							builder.add(dcEntry);
615 						}
616 					}
617 				}
618 				String content = cfg.toText();
619 
620 				// create a new DirCacheEntry for .gitmodules file.
621 				final DirCacheEntry dcEntry = new DirCacheEntry(Constants.DOT_GIT_MODULES);
622 				ObjectId objectId = inserter.insert(Constants.OBJ_BLOB,
623 						content.getBytes(UTF_8));
624 				dcEntry.setObjectId(objectId);
625 				dcEntry.setFileMode(FileMode.REGULAR_FILE);
626 				builder.add(dcEntry);
627 
628 				if (recordSubmoduleLabels) {
629 					// create a new DirCacheEntry for .gitattributes file.
630 					final DirCacheEntry dcEntryAttr = new DirCacheEntry(Constants.DOT_GIT_ATTRIBUTES);
631 					ObjectId attrId = inserter.insert(Constants.OBJ_BLOB,
632 							attributes.toString().getBytes(UTF_8));
633 					dcEntryAttr.setObjectId(attrId);
634 					dcEntryAttr.setFileMode(FileMode.REGULAR_FILE);
635 					builder.add(dcEntryAttr);
636 				}
637 
638 				builder.finish();
639 				ObjectId treeId = index.writeTree(inserter);
640 
641 				// Create a Commit object, populate it and write it
642 				ObjectId headId = repo.resolve(targetBranch + "^{commit}"); //$NON-NLS-1$
643 				if (headId != null && rw.parseCommit(headId).getTree().getId().equals(treeId)) {
644 					// No change. Do nothing.
645 					return rw.parseCommit(headId);
646 				}
647 
648 				CommitBuilder commit = new CommitBuilder();
649 				commit.setTreeId(treeId);
650 				if (headId != null)
651 					commit.setParentIds(headId);
652 				commit.setAuthor(author);
653 				commit.setCommitter(author);
654 				commit.setMessage(RepoText.get().repoCommitMessage);
655 
656 				ObjectId commitId = inserter.insert(commit);
657 				inserter.flush();
658 
659 				RefUpdate ru = repo.updateRef(targetBranch);
660 				ru.setNewObjectId(commitId);
661 				ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId());
662 				Result rc = ru.update(rw);
663 
664 				switch (rc) {
665 					case NEW:
666 					case FORCED:
667 					case FAST_FORWARD:
668 						// Successful. Do nothing.
669 						break;
670 					case REJECTED:
671 					case LOCK_FAILURE:
672 						throw new ConcurrentRefUpdateException(
673 								MessageFormat.format(
674 										JGitText.get().cannotLock, targetBranch),
675 								ru.getRef(),
676 								rc);
677 					default:
678 						throw new JGitInternalException(MessageFormat.format(
679 								JGitText.get().updatingRefFailed,
680 								targetBranch, commitId.name(), rc));
681 				}
682 
683 				return rw.parseCommit(commitId);
684 			} catch (GitAPIException | IOException e) {
685 				throw new ManifestErrorException(e);
686 			}
687 		} else {
688 			try (Git git = new Git(repo)) {
689 				for (RepoProject proj : filteredProjects) {
690 					addSubmodule(proj.getName(), proj.getUrl(), proj.getPath(),
691 							proj.getRevision(), proj.getCopyFiles(),
692 							proj.getLinkFiles(), git);
693 				}
694 				return git.commit().setMessage(RepoText.get().repoCommitMessage)
695 						.call();
696 			} catch (GitAPIException | IOException e) {
697 				throw new ManifestErrorException(e);
698 			}
699 		}
700 	}
701 
702 	private void addSubmodule(String name, String url, String path,
703 			String revision, List<CopyFile> copyfiles, List<LinkFile> linkfiles,
704 			Git git) throws GitAPIException, IOException {
705 		assert (!repo.isBare());
706 		assert (git != null);
707 		if (!linkfiles.isEmpty()) {
708 			throw new UnsupportedOperationException(
709 					JGitText.get().nonBareLinkFilesNotSupported);
710 		}
711 
712 		SubmoduleAddCommand add = git.submoduleAdd().setName(name).setPath(path)
713 				.setURI(url);
714 		if (monitor != null)
715 			add.setProgressMonitor(monitor);
716 
717 		Repository subRepo = add.call();
718 		if (revision != null) {
719 			try (Git sub = new Git(subRepo)) {
720 				sub.checkout().setName(findRef(revision, subRepo)).call();
721 			}
722 			subRepo.close();
723 			git.add().addFilepattern(path).call();
724 		}
725 		for (CopyFile copyfile : copyfiles) {
726 			copyfile.copy();
727 			git.add().addFilepattern(copyfile.dest).call();
728 		}
729 	}
730 
731 	/**
732 	 * Rename the projects if there's a conflict when converted to submodules.
733 	 *
734 	 * @param projects
735 	 *            parsed projects
736 	 * @return projects that are renamed if necessary
737 	 */
738 	private List<RepoProject> renameProjects(List<RepoProject> projects) {
739 		Map<String, List<RepoProject>> m = new TreeMap<>();
740 		for (RepoProject proj : projects) {
741 			List<RepoProject> l = m.get(proj.getName());
742 			if (l == null) {
743 				l = new ArrayList<>();
744 				m.put(proj.getName(), l);
745 			}
746 			l.add(proj);
747 		}
748 
749 		List<RepoProject> ret = new ArrayList<>();
750 		for (List<RepoProject> ps : m.values()) {
751 			boolean nameConflict = ps.size() != 1;
752 			for (RepoProject proj : ps) {
753 				String name = proj.getName();
754 				if (nameConflict) {
755 					name += SLASH + proj.getPath();
756 				}
757 				RepoProject p = new RepoProject(name,
758 						proj.getPath(), proj.getRevision(), null,
759 						proj.getGroups(), proj.getRecommendShallow());
760 				p.setUrl(proj.getUrl());
761 				p.addCopyFiles(proj.getCopyFiles());
762 				p.addLinkFiles(proj.getLinkFiles());
763 				ret.add(p);
764 			}
765 		}
766 		return ret;
767 	}
768 
769 	/*
770 	 * Assume we are document "a/b/index.html", what should we put in a href to get to "a/" ?
771 	 * Returns the child if either base or child is not a bare path. This provides a missing feature in
772 	 * java.net.URI (see http://bugs.java.com/view_bug.do?bug_id=6226081).
773 	 */
774 	private static final String SLASH = "/"; //$NON-NLS-1$
775 	static URI relativize(URI current, URI target) {
776 		if (!Objects.equals(current.getHost(), target.getHost())) {
777 			return target;
778 		}
779 
780 		String cur = current.normalize().getPath();
781 		String dest = target.normalize().getPath();
782 
783 		// TODO(hanwen): maybe (absolute, relative) should throw an exception.
784 		if (cur.startsWith(SLASH) != dest.startsWith(SLASH)) {
785 			return target;
786 		}
787 
788 		while (cur.startsWith(SLASH)) {
789 			cur = cur.substring(1);
790 		}
791 		while (dest.startsWith(SLASH)) {
792 			dest = dest.substring(1);
793 		}
794 
795 		if (cur.indexOf('/') == -1 || dest.indexOf('/') == -1) {
796 			// Avoid having to special-casing in the next two ifs.
797 			String prefix = "prefix/"; //$NON-NLS-1$
798 			cur = prefix + cur;
799 			dest = prefix + dest;
800 		}
801 
802 		if (!cur.endsWith(SLASH)) {
803 			// The current file doesn't matter.
804 			int lastSlash = cur.lastIndexOf('/');
805 			cur = cur.substring(0, lastSlash);
806 		}
807 		String destFile = ""; //$NON-NLS-1$
808 		if (!dest.endsWith(SLASH)) {
809 			// We always have to provide the destination file.
810 			int lastSlash = dest.lastIndexOf('/');
811 			destFile = dest.substring(lastSlash + 1, dest.length());
812 			dest = dest.substring(0, dest.lastIndexOf('/'));
813 		}
814 
815 		String[] cs = cur.split(SLASH);
816 		String[] ds = dest.split(SLASH);
817 
818 		int common = 0;
819 		while (common < cs.length && common < ds.length && cs[common].equals(ds[common])) {
820 			common++;
821 		}
822 
823 		StringJoiner j = new StringJoiner(SLASH);
824 		for (int i = common; i < cs.length; i++) {
825 			j.add(".."); //$NON-NLS-1$
826 		}
827 		for (int i = common; i < ds.length; i++) {
828 			j.add(ds[i]);
829 		}
830 
831 		j.add(destFile);
832 		return URI.create(j.toString());
833 	}
834 
835 	private static String findRef(String ref, Repository repo)
836 			throws IOException {
837 		if (!ObjectId.isId(ref)) {
838 			Ref r = repo.exactRef(R_REMOTES + DEFAULT_REMOTE_NAME + "/" + ref); //$NON-NLS-1$
839 			if (r != null)
840 				return r.getName();
841 		}
842 		return ref;
843 	}
844 }