View Javadoc
1   /*
2    * Copyright (C) 2011, GitHub 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.submodule;
44  
45  import java.io.File;
46  import java.io.IOException;
47  import java.text.MessageFormat;
48  import java.util.Locale;
49  
50  import org.eclipse.jgit.dircache.DirCache;
51  import org.eclipse.jgit.dircache.DirCacheIterator;
52  import org.eclipse.jgit.errors.ConfigInvalidException;
53  import org.eclipse.jgit.errors.CorruptObjectException;
54  import org.eclipse.jgit.errors.IncorrectObjectTypeException;
55  import org.eclipse.jgit.errors.MissingObjectException;
56  import org.eclipse.jgit.errors.RepositoryNotFoundException;
57  import org.eclipse.jgit.internal.JGitText;
58  import org.eclipse.jgit.lib.AnyObjectId;
59  import org.eclipse.jgit.lib.BlobBasedConfig;
60  import org.eclipse.jgit.lib.Config;
61  import org.eclipse.jgit.lib.ConfigConstants;
62  import org.eclipse.jgit.lib.Constants;
63  import org.eclipse.jgit.lib.FileMode;
64  import org.eclipse.jgit.lib.ObjectId;
65  import org.eclipse.jgit.lib.Ref;
66  import org.eclipse.jgit.lib.Repository;
67  import org.eclipse.jgit.lib.RepositoryBuilder;
68  import org.eclipse.jgit.lib.StoredConfig;
69  import org.eclipse.jgit.storage.file.FileBasedConfig;
70  import org.eclipse.jgit.treewalk.AbstractTreeIterator;
71  import org.eclipse.jgit.treewalk.CanonicalTreeParser;
72  import org.eclipse.jgit.treewalk.TreeWalk;
73  import org.eclipse.jgit.treewalk.filter.PathFilter;
74  import org.eclipse.jgit.treewalk.filter.TreeFilter;
75  import org.eclipse.jgit.util.FS;
76  
77  /**
78   * Walker that visits all submodule entries found in a tree
79   */
80  public class SubmoduleWalk implements AutoCloseable {
81  
82  	/**
83  	 * The values for the config param submodule.<name>.ignore
84  	 *
85  	 * @since 3.6
86  	 */
87  	public enum IgnoreSubmoduleMode {
88  		/**
89  		 * Ignore all modifications to submodules
90  		 */
91  		ALL,
92  
93  		/**
94  		 * Ignore changes to the working tree of a submodule
95  		 */
96  		DIRTY,
97  
98  		/**
99  		 * Ignore changes to untracked files in the working tree of a submodule
100 		 */
101 		UNTRACKED,
102 
103 		/**
104 		 * Ignore nothing. That's the default
105 		 */
106 		NONE;
107 	}
108 
109 	/**
110 	 * Create a generator to walk over the submodule entries currently in the
111 	 * index
112 	 *
113 	 * The {@code .gitmodules} file is read from the index.
114 	 *
115 	 * @param repository
116 	 * @return generator over submodule index entries
117 	 * @throws IOException
118 	 */
119 	public static SubmoduleWalk forIndex(Repository repository)
120 			throws IOException {
121 		SubmoduleWalk generator = new SubmoduleWalk(repository);
122 		try {
123 			DirCache index = repository.readDirCache();
124 			generator.setTree(new DirCacheIterator(index));
125 		} catch (IOException e) {
126 			generator.close();
127 			throw e;
128 		}
129 		return generator;
130 	}
131 
132 	/**
133 	 * Create a generator and advance it to the submodule entry at the given
134 	 * path
135 	 *
136 	 * @param repository
137 	 * @param treeId
138 	 *            the root of a tree containing both a submodule at the given path
139 	 *            and .gitmodules at the root.
140 	 * @param path
141 	 * @return generator at given path, null if no submodule at given path
142 	 * @throws IOException
143 	 */
144 	public static SubmoduleWalk forPath(Repository repository,
145 			AnyObjectId treeId, String path) throws IOException {
146 		SubmoduleWalk generator = new SubmoduleWalk(repository);
147 		try {
148 			generator.setTree(treeId);
149 			PathFilter filter = PathFilter.create(path);
150 			generator.setFilter(filter);
151 			generator.setRootTree(treeId);
152 			while (generator.next())
153 				if (filter.isDone(generator.walk))
154 					return generator;
155 		} catch (IOException e) {
156 			generator.close();
157 			throw e;
158 		}
159 		generator.close();
160 		return null;
161 	}
162 
163 	/**
164 	 * Create a generator and advance it to the submodule entry at the given
165 	 * path
166 	 *
167 	 * @param repository
168 	 * @param iterator
169 	 *            the root of a tree containing both a submodule at the given path
170 	 *            and .gitmodules at the root.
171 	 * @param path
172 	 * @return generator at given path, null if no submodule at given path
173 	 * @throws IOException
174 	 */
175 	public static SubmoduleWalk forPath(Repository repository,
176 			AbstractTreeIterator iterator, String path) throws IOException {
177 		SubmoduleWalk generator = new SubmoduleWalk(repository);
178 		try {
179 			generator.setTree(iterator);
180 			PathFilter filter = PathFilter.create(path);
181 			generator.setFilter(filter);
182 			generator.setRootTree(iterator);
183 			while (generator.next())
184 				if (filter.isDone(generator.walk))
185 					return generator;
186 		} catch (IOException e) {
187 			generator.close();
188 			throw e;
189 		}
190 		generator.close();
191 		return null;
192 	}
193 
194 	/**
195 	 * Get submodule directory
196 	 *
197 	 * @param parent
198 	 * @param path
199 	 * @return directory
200 	 */
201 	public static File getSubmoduleDirectory(final Repository parent,
202 			final String path) {
203 		return new File(parent.getWorkTree(), path);
204 	}
205 
206 	/**
207 	 * Get submodule repository
208 	 *
209 	 * @param parent
210 	 * @param path
211 	 * @return repository or null if repository doesn't exist
212 	 * @throws IOException
213 	 */
214 	public static Repository getSubmoduleRepository(final Repository parent,
215 			final String path) throws IOException {
216 		return getSubmoduleRepository(parent.getWorkTree(), path);
217 	}
218 
219 	/**
220 	 * Get submodule repository at path
221 	 *
222 	 * @param parent
223 	 * @param path
224 	 * @return repository or null if repository doesn't exist
225 	 * @throws IOException
226 	 */
227 	public static Repository getSubmoduleRepository(final File parent,
228 			final String path) throws IOException {
229 		File subWorkTree = new File(parent, path);
230 		if (!subWorkTree.isDirectory())
231 			return null;
232 		File workTree = new File(parent, path);
233 		try {
234 			return new RepositoryBuilder() //
235 					.setMustExist(true) //
236 					.setFS(FS.DETECTED) //
237 					.setWorkTree(workTree) //
238 					.build();
239 		} catch (RepositoryNotFoundException e) {
240 			return null;
241 		}
242 	}
243 
244 	/**
245 	 * Resolve submodule repository URL.
246 	 * <p>
247 	 * This handles relative URLs that are typically specified in the
248 	 * '.gitmodules' file by resolving them against the remote URL of the parent
249 	 * repository.
250 	 * <p>
251 	 * Relative URLs will be resolved against the parent repository's working
252 	 * directory if the parent repository has no configured remote URL.
253 	 *
254 	 * @param parent
255 	 *            parent repository
256 	 * @param url
257 	 *            absolute or relative URL of the submodule repository
258 	 * @return resolved URL
259 	 * @throws IOException
260 	 */
261 	public static String getSubmoduleRemoteUrl(final Repository parent,
262 			final String url) throws IOException {
263 		if (!url.startsWith("./") && !url.startsWith("../")) //$NON-NLS-1$ //$NON-NLS-2$
264 			return url;
265 
266 		String remoteName = null;
267 		// Look up remote URL associated wit HEAD ref
268 		Ref ref = parent.exactRef(Constants.HEAD);
269 		if (ref != null) {
270 			if (ref.isSymbolic())
271 				ref = ref.getLeaf();
272 			remoteName = parent.getConfig().getString(
273 					ConfigConstants.CONFIG_BRANCH_SECTION,
274 					Repository.shortenRefName(ref.getName()),
275 					ConfigConstants.CONFIG_KEY_REMOTE);
276 		}
277 
278 		// Fall back to 'origin' if current HEAD ref has no remote URL
279 		if (remoteName == null)
280 			remoteName = Constants.DEFAULT_REMOTE_NAME;
281 
282 		String remoteUrl = parent.getConfig().getString(
283 				ConfigConstants.CONFIG_REMOTE_SECTION, remoteName,
284 				ConfigConstants.CONFIG_KEY_URL);
285 
286 		// Fall back to parent repository's working directory if no remote URL
287 		if (remoteUrl == null) {
288 			remoteUrl = parent.getWorkTree().getAbsolutePath();
289 			// Normalize slashes to '/'
290 			if ('\\' == File.separatorChar)
291 				remoteUrl = remoteUrl.replace('\\', '/');
292 		}
293 
294 		// Remove trailing '/'
295 		if (remoteUrl.charAt(remoteUrl.length() - 1) == '/')
296 			remoteUrl = remoteUrl.substring(0, remoteUrl.length() - 1);
297 
298 		char separator = '/';
299 		String submoduleUrl = url;
300 		while (submoduleUrl.length() > 0) {
301 			if (submoduleUrl.startsWith("./")) //$NON-NLS-1$
302 				submoduleUrl = submoduleUrl.substring(2);
303 			else if (submoduleUrl.startsWith("../")) { //$NON-NLS-1$
304 				int lastSeparator = remoteUrl.lastIndexOf('/');
305 				if (lastSeparator < 1) {
306 					lastSeparator = remoteUrl.lastIndexOf(':');
307 					separator = ':';
308 				}
309 				if (lastSeparator < 1)
310 					throw new IOException(MessageFormat.format(
311 							JGitText.get().submoduleParentRemoteUrlInvalid,
312 							remoteUrl));
313 				remoteUrl = remoteUrl.substring(0, lastSeparator);
314 				submoduleUrl = submoduleUrl.substring(3);
315 			} else
316 				break;
317 		}
318 		return remoteUrl + separator + submoduleUrl;
319 	}
320 
321 	private final Repository repository;
322 
323 	private final TreeWalk walk;
324 
325 	private StoredConfig repoConfig;
326 
327 	private AbstractTreeIterator rootTree;
328 
329 	private Config modulesConfig;
330 
331 	private String path;
332 
333 	/**
334 	 * Create submodule generator
335 	 *
336 	 * @param repository
337 	 * @throws IOException
338 	 */
339 	public SubmoduleWalk(final Repository repository) throws IOException {
340 		this.repository = repository;
341 		repoConfig = repository.getConfig();
342 		walk = new TreeWalk(repository);
343 		walk.setRecursive(true);
344 	}
345 
346 	/**
347 	 * Set the config used by this walk.
348 	 *
349 	 * This method need only be called if constructing a walk manually instead of
350 	 * with one of the static factory methods above.
351 	 *
352 	 * @param config
353 	 *            .gitmodules config object
354 	 * @return this generator
355 	 */
356 	public SubmoduleWalk setModulesConfig(final Config config) {
357 		modulesConfig = config;
358 		return this;
359 	}
360 
361 	/**
362 	 * Set the tree used by this walk for finding {@code .gitmodules}.
363 	 * <p>
364 	 * The root tree is not read until the first submodule is encountered by the
365 	 * walk.
366 	 * <p>
367 	 * This method need only be called if constructing a walk manually instead of
368 	 * with one of the static factory methods above.
369 	 *
370 	 * @param tree
371 	 *            tree containing .gitmodules
372 	 * @return this generator
373 	 */
374 	public SubmoduleWalk setRootTree(final AbstractTreeIterator tree) {
375 		rootTree = tree;
376 		modulesConfig = null;
377 		return this;
378 	}
379 
380 	/**
381 	 * Set the tree used by this walk for finding {@code .gitmodules}.
382 	 * <p>
383 	 * The root tree is not read until the first submodule is encountered by the
384 	 * walk.
385 	 * <p>
386 	 * This method need only be called if constructing a walk manually instead of
387 	 * with one of the static factory methods above.
388 	 *
389 	 * @param id
390 	 *            ID of a tree containing .gitmodules
391 	 * @return this generator
392 	 * @throws IOException
393 	 */
394 	public SubmoduleWalk setRootTree(final AnyObjectId id) throws IOException {
395 		final CanonicalTreeParser p = new CanonicalTreeParser();
396 		p.reset(walk.getObjectReader(), id);
397 		rootTree = p;
398 		modulesConfig = null;
399 		return this;
400 	}
401 
402 	/**
403 	 * Load the config for this walk from {@code .gitmodules}.
404 	 * <p>
405 	 * Uses the root tree if {@link #setRootTree(AbstractTreeIterator)} was
406 	 * previously called, otherwise uses the working tree.
407 	 * <p>
408 	 * If no submodule config is found, loads an empty config.
409 	 *
410 	 * @return this generator
411 	 * @throws IOException if an error occurred, or if the repository is bare
412 	 * @throws ConfigInvalidException
413 	 */
414 	public SubmoduleWalk loadModulesConfig() throws IOException, ConfigInvalidException {
415 		if (rootTree == null) {
416 			File modulesFile = new File(repository.getWorkTree(),
417 					Constants.DOT_GIT_MODULES);
418 			FileBasedConfig config = new FileBasedConfig(modulesFile,
419 					repository.getFS());
420 			config.load();
421 			modulesConfig = config;
422 		} else {
423 			try (TreeWalk configWalk = new TreeWalk(repository)) {
424 				configWalk.addTree(rootTree);
425 
426 				// The root tree may be part of the submodule walk, so we need to revert
427 				// it after this walk.
428 				int idx;
429 				for (idx = 0; !rootTree.first(); idx++) {
430 					rootTree.back(1);
431 				}
432 
433 				try {
434 					configWalk.setRecursive(false);
435 					PathFilter filter = PathFilter.create(Constants.DOT_GIT_MODULES);
436 					configWalk.setFilter(filter);
437 					while (configWalk.next()) {
438 						if (filter.isDone(configWalk)) {
439 							modulesConfig = new BlobBasedConfig(null, repository,
440 									configWalk.getObjectId(0));
441 							return this;
442 						}
443 					}
444 					modulesConfig = new Config();
445 				} finally {
446 					if (idx > 0)
447 						rootTree.next(idx);
448 				}
449 			}
450 		}
451 		return this;
452 	}
453 
454 	/**
455 	 * Checks whether the working tree contains a .gitmodules file. That's a
456 	 * hint that the repo contains submodules.
457 	 *
458 	 * @param repository
459 	 *            the repository to check
460 	 * @return <code>true</code> if the working tree contains a .gitmodules file,
461 	 *         <code>false</code> otherwise. Always returns <code>false</code>
462 	 *         for bare repositories.
463 	 * @throws IOException
464 	 * @throws CorruptObjectException
465 	 * @since 3.6
466 	 */
467 	public static boolean containsGitModulesFile(Repository repository)
468 			throws IOException {
469 		if (repository.isBare()) {
470 			return false;
471 		}
472 		File modulesFile = new File(repository.getWorkTree(),
473 				Constants.DOT_GIT_MODULES);
474 		return (modulesFile.exists());
475 	}
476 
477 	private void lazyLoadModulesConfig() throws IOException, ConfigInvalidException {
478 		if (modulesConfig == null)
479 			loadModulesConfig();
480 	}
481 
482 	/**
483 	 * Set tree filter
484 	 *
485 	 * @param filter
486 	 * @return this generator
487 	 */
488 	public SubmoduleWalk setFilter(TreeFilter filter) {
489 		walk.setFilter(filter);
490 		return this;
491 	}
492 
493 	/**
494 	 * Set the tree iterator used for finding submodule entries
495 	 *
496 	 * @param iterator
497 	 * @return this generator
498 	 * @throws CorruptObjectException
499 	 */
500 	public SubmoduleWalk setTree(final AbstractTreeIterator iterator)
501 			throws CorruptObjectException {
502 		walk.addTree(iterator);
503 		return this;
504 	}
505 
506 	/**
507 	 * Set the tree used for finding submodule entries
508 	 *
509 	 * @param treeId
510 	 * @return this generator
511 	 * @throws IOException
512 	 * @throws IncorrectObjectTypeException
513 	 * @throws MissingObjectException
514 	 */
515 	public SubmoduleWalk setTree(final AnyObjectId treeId) throws IOException {
516 		walk.addTree(treeId);
517 		return this;
518 	}
519 
520 	/**
521 	 * Reset generator and start new submodule walk
522 	 *
523 	 * @return this generator
524 	 */
525 	public SubmoduleWalk reset() {
526 		repoConfig = repository.getConfig();
527 		modulesConfig = null;
528 		walk.reset();
529 		return this;
530 	}
531 
532 	/**
533 	 * Get directory that will be the root of the submodule's local repository
534 	 *
535 	 * @return submodule repository directory
536 	 */
537 	public File getDirectory() {
538 		return getSubmoduleDirectory(repository, path);
539 	}
540 
541 	/**
542 	 * Advance to next submodule in the index tree.
543 	 *
544 	 * The object id and path of the next entry can be obtained by calling
545 	 * {@link #getObjectId()} and {@link #getPath()}.
546 	 *
547 	 * @return true if entry found, false otherwise
548 	 * @throws IOException
549 	 */
550 	public boolean next() throws IOException {
551 		while (walk.next()) {
552 			if (FileMode.GITLINK != walk.getFileMode(0))
553 				continue;
554 			path = walk.getPathString();
555 			return true;
556 		}
557 		path = null;
558 		return false;
559 	}
560 
561 	/**
562 	 * Get path of current submodule entry
563 	 *
564 	 * @return path
565 	 */
566 	public String getPath() {
567 		return path;
568 	}
569 
570 	/**
571 	 * Get object id of current submodule entry
572 	 *
573 	 * @return object id
574 	 */
575 	public ObjectId getObjectId() {
576 		return walk.getObjectId(0);
577 	}
578 
579 	/**
580 	 * Get the configured path for current entry. This will be the value from
581 	 * the .gitmodules file in the current repository's working tree.
582 	 *
583 	 * @return configured path
584 	 * @throws ConfigInvalidException
585 	 * @throws IOException
586 	 */
587 	public String getModulesPath() throws IOException, ConfigInvalidException {
588 		lazyLoadModulesConfig();
589 		return modulesConfig.getString(
590 				ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
591 				ConfigConstants.CONFIG_KEY_PATH);
592 	}
593 
594 	/**
595 	 * Get the configured remote URL for current entry. This will be the value
596 	 * from the repository's config.
597 	 *
598 	 * @return configured URL
599 	 * @throws ConfigInvalidException
600 	 * @throws IOException
601 	 */
602 	public String getConfigUrl() throws IOException, ConfigInvalidException {
603 		return repoConfig.getString(ConfigConstants.CONFIG_SUBMODULE_SECTION,
604 				path, ConfigConstants.CONFIG_KEY_URL);
605 	}
606 
607 	/**
608 	 * Get the configured remote URL for current entry. This will be the value
609 	 * from the .gitmodules file in the current repository's working tree.
610 	 *
611 	 * @return configured URL
612 	 * @throws ConfigInvalidException
613 	 * @throws IOException
614 	 */
615 	public String getModulesUrl() throws IOException, ConfigInvalidException {
616 		lazyLoadModulesConfig();
617 		return modulesConfig.getString(
618 				ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
619 				ConfigConstants.CONFIG_KEY_URL);
620 	}
621 
622 	/**
623 	 * Get the configured update field for current entry. This will be the value
624 	 * from the repository's config.
625 	 *
626 	 * @return update value
627 	 * @throws ConfigInvalidException
628 	 * @throws IOException
629 	 */
630 	public String getConfigUpdate() throws IOException, ConfigInvalidException {
631 		return repoConfig.getString(ConfigConstants.CONFIG_SUBMODULE_SECTION,
632 				path, ConfigConstants.CONFIG_KEY_UPDATE);
633 	}
634 
635 	/**
636 	 * Get the configured update field for current entry. This will be the value
637 	 * from the .gitmodules file in the current repository's working tree.
638 	 *
639 	 * @return update value
640 	 * @throws ConfigInvalidException
641 	 * @throws IOException
642 	 */
643 	public String getModulesUpdate() throws IOException, ConfigInvalidException {
644 		lazyLoadModulesConfig();
645 		return modulesConfig.getString(
646 				ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
647 				ConfigConstants.CONFIG_KEY_UPDATE);
648 	}
649 
650 	/**
651 	 * Get the configured ignore field for the current entry. This will be the
652 	 * value from the .gitmodules file in the current repository's working tree.
653 	 *
654 	 * @return ignore value
655 	 * @throws ConfigInvalidException
656 	 * @throws IOException
657 	 * @since 3.6
658 	 */
659 	public IgnoreSubmoduleMode getModulesIgnore() throws IOException,
660 			ConfigInvalidException {
661 		lazyLoadModulesConfig();
662 		String name = modulesConfig.getString(
663 				ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
664 				ConfigConstants.CONFIG_KEY_IGNORE);
665 		if (name == null)
666 			return null;
667 		return IgnoreSubmoduleMode
668 				.valueOf(name.trim().toUpperCase(Locale.ROOT));
669 	}
670 
671 	/**
672 	 * Get repository for current submodule entry
673 	 *
674 	 * @return repository or null if non-existent
675 	 * @throws IOException
676 	 */
677 	public Repository getRepository() throws IOException {
678 		return getSubmoduleRepository(repository, path);
679 	}
680 
681 	/**
682 	 * Get commit id that HEAD points to in the current submodule's repository
683 	 *
684 	 * @return object id of HEAD reference
685 	 * @throws IOException
686 	 */
687 	public ObjectId getHead() throws IOException {
688 		Repository subRepo = getRepository();
689 		if (subRepo == null)
690 			return null;
691 		try {
692 			return subRepo.resolve(Constants.HEAD);
693 		} finally {
694 			subRepo.close();
695 		}
696 	}
697 
698 	/**
699 	 * Get ref that HEAD points to in the current submodule's repository
700 	 *
701 	 * @return ref name, null on failures
702 	 * @throws IOException
703 	 */
704 	public String getHeadRef() throws IOException {
705 		Repository subRepo = getRepository();
706 		if (subRepo == null)
707 			return null;
708 		try {
709 			Ref head = subRepo.exactRef(Constants.HEAD);
710 			return head != null ? head.getLeaf().getName() : null;
711 		} finally {
712 			subRepo.close();
713 		}
714 	}
715 
716 	/**
717 	 * Get the resolved remote URL for the current submodule.
718 	 * <p>
719 	 * This method resolves the value of {@link #getModulesUrl()} to an absolute
720 	 * URL
721 	 *
722 	 * @return resolved remote URL
723 	 * @throws IOException
724 	 * @throws ConfigInvalidException
725 	 */
726 	public String getRemoteUrl() throws IOException, ConfigInvalidException {
727 		String url = getModulesUrl();
728 		return url != null ? getSubmoduleRemoteUrl(repository, url) : null;
729 	}
730 
731 	/**
732 	 * Release any resources used by this walker's reader.
733 	 *
734 	 * @since 4.0
735 	 */
736 	@Override
737 	public void close() {
738 		walk.close();
739 	}
740 }