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