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