1 /*
2 * Copyright (C) 2010, 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
11 package org.eclipse.jgit.lib;
12
13 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_CORE_SECTION;
14 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BARE;
15 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_WORKTREE;
16 import static org.eclipse.jgit.lib.Constants.DOT_GIT;
17 import static org.eclipse.jgit.lib.Constants.GIT_ALTERNATE_OBJECT_DIRECTORIES_KEY;
18 import static org.eclipse.jgit.lib.Constants.GIT_CEILING_DIRECTORIES_KEY;
19 import static org.eclipse.jgit.lib.Constants.GIT_DIR_KEY;
20 import static org.eclipse.jgit.lib.Constants.GIT_INDEX_FILE_KEY;
21 import static org.eclipse.jgit.lib.Constants.GIT_OBJECT_DIRECTORY_KEY;
22 import static org.eclipse.jgit.lib.Constants.GIT_WORK_TREE_KEY;
23
24 import java.io.File;
25 import java.io.IOException;
26 import java.text.MessageFormat;
27 import java.util.Collection;
28 import java.util.LinkedList;
29 import java.util.List;
30
31 import org.eclipse.jgit.errors.ConfigInvalidException;
32 import org.eclipse.jgit.errors.RepositoryNotFoundException;
33 import org.eclipse.jgit.internal.JGitText;
34 import org.eclipse.jgit.internal.storage.file.FileRepository;
35 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
36 import org.eclipse.jgit.storage.file.FileBasedConfig;
37 import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
38 import org.eclipse.jgit.util.FS;
39 import org.eclipse.jgit.util.IO;
40 import org.eclipse.jgit.util.RawParseUtils;
41 import org.eclipse.jgit.util.SystemReader;
42
43 /**
44 * Base builder to customize repository construction.
45 * <p>
46 * Repository implementations may subclass this builder in order to add custom
47 * repository detection methods.
48 *
49 * @param <B>
50 * type of the repository builder.
51 * @param <R>
52 * type of the repository that is constructed.
53 * @see RepositoryBuilder
54 * @see FileRepositoryBuilder
55 */
56 public class BaseRepositoryBuilder<B extends BaseRepositoryBuilder, R extends Repository> {
57 private static boolean isSymRef(byte[] ref) {
58 if (ref.length < 9)
59 return false;
60 return /**/ref[0] == 'g' //
61 && ref[1] == 'i' //
62 && ref[2] == 't' //
63 && ref[3] == 'd' //
64 && ref[4] == 'i' //
65 && ref[5] == 'r' //
66 && ref[6] == ':' //
67 && ref[7] == ' ';
68 }
69
70 private static File getSymRef(File workTree, File dotGit, FS fs)
71 throws IOException {
72 byte[] content = IO.readFully(dotGit);
73 if (!isSymRef(content)) {
74 throw new IOException(MessageFormat.format(
75 JGitText.get().invalidGitdirRef, dotGit.getAbsolutePath()));
76 }
77
78 int pathStart = 8;
79 int lineEnd = RawParseUtils.nextLF(content, pathStart);
80 while (content[lineEnd - 1] == '\n' ||
81 (content[lineEnd - 1] == '\r'
82 && SystemReader.getInstance().isWindows())) {
83 lineEnd--;
84 }
85 if (lineEnd == pathStart) {
86 throw new IOException(MessageFormat.format(
87 JGitText.get().invalidGitdirRef, dotGit.getAbsolutePath()));
88 }
89
90 String gitdirPath = RawParseUtils.decode(content, pathStart, lineEnd);
91 File gitdirFile = fs.resolve(workTree, gitdirPath);
92 if (gitdirFile.isAbsolute()) {
93 return gitdirFile;
94 }
95 return new File(workTree, gitdirPath).getCanonicalFile();
96 }
97
98 private FS fs;
99
100 private File gitDir;
101
102 private File objectDirectory;
103
104 private List<File> alternateObjectDirectories;
105
106 private File indexFile;
107
108 private File workTree;
109
110 /** Directories limiting the search for a Git repository. */
111 private List<File> ceilingDirectories;
112
113 /** True only if the caller wants to force bare behavior. */
114 private boolean bare;
115
116 /** True if the caller requires the repository to exist. */
117 private boolean mustExist;
118
119 /** Configuration file of target repository, lazily loaded if required. */
120 private Config config;
121
122 /**
123 * Set the file system abstraction needed by this repository.
124 *
125 * @param fs
126 * the abstraction.
127 * @return {@code this} (for chaining calls).
128 */
129 public B setFS(FS fs) {
130 this.fs = fs;
131 return self();
132 }
133
134 /**
135 * Get the file system abstraction, or null if not set.
136 *
137 * @return the file system abstraction, or null if not set.
138 */
139 public FS getFS() {
140 return fs;
141 }
142
143 /**
144 * Set the Git directory storing the repository metadata.
145 * <p>
146 * The meta directory stores the objects, references, and meta files like
147 * {@code MERGE_HEAD}, or the index file. If {@code null} the path is
148 * assumed to be {@code workTree/.git}.
149 *
150 * @param gitDir
151 * {@code GIT_DIR}, the repository meta directory.
152 * @return {@code this} (for chaining calls).
153 */
154 public B setGitDir(File gitDir) {
155 this.gitDir = gitDir;
156 this.config = null;
157 return self();
158 }
159
160 /**
161 * Get the meta data directory; null if not set.
162 *
163 * @return the meta data directory; null if not set.
164 */
165 public File getGitDir() {
166 return gitDir;
167 }
168
169 /**
170 * Set the directory storing the repository's objects.
171 *
172 * @param objectDirectory
173 * {@code GIT_OBJECT_DIRECTORY}, the directory where the
174 * repository's object files are stored.
175 * @return {@code this} (for chaining calls).
176 */
177 public B setObjectDirectory(File objectDirectory) {
178 this.objectDirectory = objectDirectory;
179 return self();
180 }
181
182 /**
183 * Get the object directory; null if not set.
184 *
185 * @return the object directory; null if not set.
186 */
187 public File getObjectDirectory() {
188 return objectDirectory;
189 }
190
191 /**
192 * Add an alternate object directory to the search list.
193 * <p>
194 * This setting handles one alternate directory at a time, and is provided
195 * to support {@code GIT_ALTERNATE_OBJECT_DIRECTORIES}.
196 *
197 * @param other
198 * another objects directory to search after the standard one.
199 * @return {@code this} (for chaining calls).
200 */
201 public B addAlternateObjectDirectory(File other) {
202 if (other != null) {
203 if (alternateObjectDirectories == null)
204 alternateObjectDirectories = new LinkedList<>();
205 alternateObjectDirectories.add(other);
206 }
207 return self();
208 }
209
210 /**
211 * Add alternate object directories to the search list.
212 * <p>
213 * This setting handles several alternate directories at once, and is
214 * provided to support {@code GIT_ALTERNATE_OBJECT_DIRECTORIES}.
215 *
216 * @param inList
217 * other object directories to search after the standard one. The
218 * collection's contents is copied to an internal list.
219 * @return {@code this} (for chaining calls).
220 */
221 public B addAlternateObjectDirectories(Collection<File> inList) {
222 if (inList != null) {
223 for (File path : inList)
224 addAlternateObjectDirectory(path);
225 }
226 return self();
227 }
228
229 /**
230 * Add alternate object directories to the search list.
231 * <p>
232 * This setting handles several alternate directories at once, and is
233 * provided to support {@code GIT_ALTERNATE_OBJECT_DIRECTORIES}.
234 *
235 * @param inList
236 * other object directories to search after the standard one. The
237 * array's contents is copied to an internal list.
238 * @return {@code this} (for chaining calls).
239 */
240 public B addAlternateObjectDirectories(File[] inList) {
241 if (inList != null) {
242 for (File path : inList)
243 addAlternateObjectDirectory(path);
244 }
245 return self();
246 }
247
248 /**
249 * Get ordered array of alternate directories; null if non were set.
250 *
251 * @return ordered array of alternate directories; null if non were set.
252 */
253 public File[] getAlternateObjectDirectories() {
254 final List<File> alts = alternateObjectDirectories;
255 if (alts == null)
256 return null;
257 return alts.toArray(new File[0]);
258 }
259
260 /**
261 * Force the repository to be treated as bare (have no working directory).
262 * <p>
263 * If bare the working directory aspects of the repository won't be
264 * configured, and will not be accessible.
265 *
266 * @return {@code this} (for chaining calls).
267 */
268 public B setBare() {
269 setIndexFile(null);
270 setWorkTree(null);
271 bare = true;
272 return self();
273 }
274
275 /**
276 * Whether this repository was forced bare by {@link #setBare()}.
277 *
278 * @return true if this repository was forced bare by {@link #setBare()}.
279 */
280 public boolean isBare() {
281 return bare;
282 }
283
284 /**
285 * Require the repository to exist before it can be opened.
286 *
287 * @param mustExist
288 * true if it must exist; false if it can be missing and created
289 * after being built.
290 * @return {@code this} (for chaining calls).
291 */
292 public B setMustExist(boolean mustExist) {
293 this.mustExist = mustExist;
294 return self();
295 }
296
297 /**
298 * Whether the repository must exist before being opened.
299 *
300 * @return true if the repository must exist before being opened.
301 */
302 public boolean isMustExist() {
303 return mustExist;
304 }
305
306 /**
307 * Set the top level directory of the working files.
308 *
309 * @param workTree
310 * {@code GIT_WORK_TREE}, the working directory of the checkout.
311 * @return {@code this} (for chaining calls).
312 */
313 public B setWorkTree(File workTree) {
314 this.workTree = workTree;
315 return self();
316 }
317
318 /**
319 * Get the work tree directory, or null if not set.
320 *
321 * @return the work tree directory, or null if not set.
322 */
323 public File getWorkTree() {
324 return workTree;
325 }
326
327 /**
328 * Set the local index file that is caching checked out file status.
329 * <p>
330 * The location of the index file tracking the status information for each
331 * checked out file in {@code workTree}. This may be null to assume the
332 * default {@code gitDiir/index}.
333 *
334 * @param indexFile
335 * {@code GIT_INDEX_FILE}, the index file location.
336 * @return {@code this} (for chaining calls).
337 */
338 public B setIndexFile(File indexFile) {
339 this.indexFile = indexFile;
340 return self();
341 }
342
343 /**
344 * Get the index file location, or null if not set.
345 *
346 * @return the index file location, or null if not set.
347 */
348 public File getIndexFile() {
349 return indexFile;
350 }
351
352 /**
353 * Read standard Git environment variables and configure from those.
354 * <p>
355 * This method tries to read the standard Git environment variables, such as
356 * {@code GIT_DIR} and {@code GIT_WORK_TREE} to configure this builder
357 * instance. If an environment variable is set, it overrides the value
358 * already set in this builder.
359 *
360 * @return {@code this} (for chaining calls).
361 */
362 public B readEnvironment() {
363 return readEnvironment(SystemReader.getInstance());
364 }
365
366 /**
367 * Read standard Git environment variables and configure from those.
368 * <p>
369 * This method tries to read the standard Git environment variables, such as
370 * {@code GIT_DIR} and {@code GIT_WORK_TREE} to configure this builder
371 * instance. If a property is already set in the builder, the environment
372 * variable is not used.
373 *
374 * @param sr
375 * the SystemReader abstraction to access the environment.
376 * @return {@code this} (for chaining calls).
377 */
378 public B readEnvironment(SystemReader sr) {
379 if (getGitDir() == null) {
380 String val = sr.getenv(GIT_DIR_KEY);
381 if (val != null)
382 setGitDir(new File(val));
383 }
384
385 if (getObjectDirectory() == null) {
386 String val = sr.getenv(GIT_OBJECT_DIRECTORY_KEY);
387 if (val != null)
388 setObjectDirectory(new File(val));
389 }
390
391 if (getAlternateObjectDirectories() == null) {
392 String val = sr.getenv(GIT_ALTERNATE_OBJECT_DIRECTORIES_KEY);
393 if (val != null) {
394 for (String path : val.split(File.pathSeparator))
395 addAlternateObjectDirectory(new File(path));
396 }
397 }
398
399 if (getWorkTree() == null) {
400 String val = sr.getenv(GIT_WORK_TREE_KEY);
401 if (val != null)
402 setWorkTree(new File(val));
403 }
404
405 if (getIndexFile() == null) {
406 String val = sr.getenv(GIT_INDEX_FILE_KEY);
407 if (val != null)
408 setIndexFile(new File(val));
409 }
410
411 if (ceilingDirectories == null) {
412 String val = sr.getenv(GIT_CEILING_DIRECTORIES_KEY);
413 if (val != null) {
414 for (String path : val.split(File.pathSeparator))
415 addCeilingDirectory(new File(path));
416 }
417 }
418
419 return self();
420 }
421
422 /**
423 * Add a ceiling directory to the search limit list.
424 * <p>
425 * This setting handles one ceiling directory at a time, and is provided to
426 * support {@code GIT_CEILING_DIRECTORIES}.
427 *
428 * @param root
429 * a path to stop searching at; its parent will not be searched.
430 * @return {@code this} (for chaining calls).
431 */
432 public B addCeilingDirectory(File root) {
433 if (root != null) {
434 if (ceilingDirectories == null)
435 ceilingDirectories = new LinkedList<>();
436 ceilingDirectories.add(root);
437 }
438 return self();
439 }
440
441 /**
442 * Add ceiling directories to the search list.
443 * <p>
444 * This setting handles several ceiling directories at once, and is provided
445 * to support {@code GIT_CEILING_DIRECTORIES}.
446 *
447 * @param inList
448 * directory paths to stop searching at. The collection's
449 * contents is copied to an internal list.
450 * @return {@code this} (for chaining calls).
451 */
452 public B addCeilingDirectories(Collection<File> inList) {
453 if (inList != null) {
454 for (File path : inList)
455 addCeilingDirectory(path);
456 }
457 return self();
458 }
459
460 /**
461 * Add ceiling directories to the search list.
462 * <p>
463 * This setting handles several ceiling directories at once, and is provided
464 * to support {@code GIT_CEILING_DIRECTORIES}.
465 *
466 * @param inList
467 * directory paths to stop searching at. The array's contents is
468 * copied to an internal list.
469 * @return {@code this} (for chaining calls).
470 */
471 public B addCeilingDirectories(File[] inList) {
472 if (inList != null) {
473 for (File path : inList)
474 addCeilingDirectory(path);
475 }
476 return self();
477 }
478
479 /**
480 * Configure {@code GIT_DIR} by searching up the file system.
481 * <p>
482 * Starts from the current working directory of the JVM and scans up through
483 * the directory tree until a Git repository is found. Success can be
484 * determined by checking for {@code getGitDir() != null}.
485 * <p>
486 * The search can be limited to specific spaces of the local filesystem by
487 * {@link #addCeilingDirectory(File)}, or inheriting the list through a
488 * prior call to {@link #readEnvironment()}.
489 *
490 * @return {@code this} (for chaining calls).
491 */
492 public B findGitDir() {
493 if (getGitDir() == null)
494 findGitDir(new File("").getAbsoluteFile()); //$NON-NLS-1$
495 return self();
496 }
497
498 /**
499 * Configure {@code GIT_DIR} by searching up the file system.
500 * <p>
501 * Starts from the supplied directory path and scans up through the parent
502 * directory tree until a Git repository is found. Success can be determined
503 * by checking for {@code getGitDir() != null}.
504 * <p>
505 * The search can be limited to specific spaces of the local filesystem by
506 * {@link #addCeilingDirectory(File)}, or inheriting the list through a
507 * prior call to {@link #readEnvironment()}.
508 *
509 * @param current
510 * directory to begin searching in.
511 * @return {@code this} (for chaining calls).
512 */
513 public B findGitDir(File current) {
514 if (getGitDir() == null) {
515 FS tryFS = safeFS();
516 while (current != null) {
517 File dir = new File(current, DOT_GIT);
518 if (FileKey.isGitRepository(dir, tryFS)) {
519 setGitDir(dir);
520 break;
521 } else if (dir.isFile()) {
522 try {
523 setGitDir(getSymRef(current, dir, tryFS));
524 break;
525 } catch (IOException ignored) {
526 // Continue searching if gitdir ref isn't found
527 }
528 } else if (FileKey.isGitRepository(current, tryFS)) {
529 setGitDir(current);
530 break;
531 }
532
533 current = current.getParentFile();
534 if (current != null && ceilingDirectories != null
535 && ceilingDirectories.contains(current))
536 break;
537 }
538 }
539 return self();
540 }
541
542 /**
543 * Guess and populate all parameters not already defined.
544 * <p>
545 * If an option was not set, the setup method will try to default the option
546 * based on other options. If insufficient information is available, an
547 * exception is thrown to the caller.
548 *
549 * @return {@code this}
550 * @throws java.lang.IllegalArgumentException
551 * insufficient parameters were set, or some parameters are
552 * incompatible with one another.
553 * @throws java.io.IOException
554 * the repository could not be accessed to configure the rest of
555 * the builder's parameters.
556 */
557 public B setup() throws IllegalArgumentException, IOException {
558 requireGitDirOrWorkTree();
559 setupGitDir();
560 setupWorkTree();
561 setupInternals();
562 return self();
563 }
564
565 /**
566 * Create a repository matching the configuration in this builder.
567 * <p>
568 * If an option was not set, the build method will try to default the option
569 * based on other options. If insufficient information is available, an
570 * exception is thrown to the caller.
571 *
572 * @return a repository matching this configuration. The caller is
573 * responsible to close the repository instance when it is no longer
574 * needed.
575 * @throws java.lang.IllegalArgumentException
576 * insufficient parameters were set.
577 * @throws java.io.IOException
578 * the repository could not be accessed to configure the rest of
579 * the builder's parameters.
580 */
581 @SuppressWarnings({ "unchecked", "resource" })
582 public R build() throws IOException {
583 R repo = (R) new FileRepository(setup());
584 if (isMustExist() && !repo.getObjectDatabase().exists())
585 throw new RepositoryNotFoundException(getGitDir());
586 return repo;
587 }
588
589 /**
590 * Require either {@code gitDir} or {@code workTree} to be set.
591 */
592 protected void requireGitDirOrWorkTree() {
593 if (getGitDir() == null && getWorkTree() == null)
594 throw new IllegalArgumentException(
595 JGitText.get().eitherGitDirOrWorkTreeRequired);
596 }
597
598 /**
599 * Perform standard gitDir initialization.
600 *
601 * @throws java.io.IOException
602 * the repository could not be accessed
603 */
604 protected void setupGitDir() throws IOException {
605 // No gitDir? Try to assume its under the workTree or a ref to another
606 // location
607 if (getGitDir() == null && getWorkTree() != null) {
608 File dotGit = new File(getWorkTree(), DOT_GIT);
609 if (!dotGit.isFile())
610 setGitDir(dotGit);
611 else
612 setGitDir(getSymRef(getWorkTree(), dotGit, safeFS()));
613 }
614 }
615
616 /**
617 * Perform standard work-tree initialization.
618 * <p>
619 * This is a method typically invoked inside of {@link #setup()}, near the
620 * end after the repository has been identified and its configuration is
621 * available for inspection.
622 *
623 * @throws java.io.IOException
624 * the repository configuration could not be read.
625 */
626 protected void setupWorkTree() throws IOException {
627 if (getFS() == null)
628 setFS(FS.DETECTED);
629
630 // If we aren't bare, we should have a work tree.
631 //
632 if (!isBare() && getWorkTree() == null)
633 setWorkTree(guessWorkTreeOrFail());
634
635 if (!isBare()) {
636 // If after guessing we're still not bare, we must have
637 // a metadata directory to hold the repository. Assume
638 // its at the work tree.
639 //
640 if (getGitDir() == null)
641 setGitDir(getWorkTree().getParentFile());
642 if (getIndexFile() == null)
643 setIndexFile(new File(getGitDir(), "index")); //$NON-NLS-1$
644 }
645 }
646
647 /**
648 * Configure the internal implementation details of the repository.
649 *
650 * @throws java.io.IOException
651 * the repository could not be accessed
652 */
653 protected void setupInternals() throws IOException {
654 if (getObjectDirectory() == null && getGitDir() != null)
655 setObjectDirectory(safeFS().resolve(getGitDir(), Constants.OBJECTS));
656 }
657
658 /**
659 * Get the cached repository configuration, loading if not yet available.
660 *
661 * @return the configuration of the repository.
662 * @throws java.io.IOException
663 * the configuration is not available, or is badly formed.
664 */
665 protected Config getConfig() throws IOException {
666 if (config == null)
667 config = loadConfig();
668 return config;
669 }
670
671 /**
672 * Parse and load the repository specific configuration.
673 * <p>
674 * The default implementation reads {@code gitDir/config}, or returns an
675 * empty configuration if gitDir was not set.
676 *
677 * @return the repository's configuration.
678 * @throws java.io.IOException
679 * the configuration is not available.
680 */
681 protected Config loadConfig() throws IOException {
682 if (getGitDir() != null) {
683 // We only want the repository's configuration file, and not
684 // the user file, as these parameters must be unique to this
685 // repository and not inherited from other files.
686 //
687 File path = safeFS().resolve(getGitDir(), Constants.CONFIG);
688 FileBasedConfig cfg = new FileBasedConfig(path, safeFS());
689 try {
690 cfg.load();
691 } catch (ConfigInvalidException err) {
692 throw new IllegalArgumentException(MessageFormat.format(
693 JGitText.get().repositoryConfigFileInvalid, path
694 .getAbsolutePath(), err.getMessage()));
695 }
696 return cfg;
697 }
698 return new Config();
699 }
700
701 private File guessWorkTreeOrFail() throws IOException {
702 final Config cfg = getConfig();
703
704 // If set, core.worktree wins.
705 //
706 String path = cfg.getString(CONFIG_CORE_SECTION, null,
707 CONFIG_KEY_WORKTREE);
708 if (path != null)
709 return safeFS().resolve(getGitDir(), path).getCanonicalFile();
710
711 // If core.bare is set, honor its value. Assume workTree is
712 // the parent directory of the repository.
713 //
714 if (cfg.getString(CONFIG_CORE_SECTION, null, CONFIG_KEY_BARE) != null) {
715 if (cfg.getBoolean(CONFIG_CORE_SECTION, CONFIG_KEY_BARE, true)) {
716 setBare();
717 return null;
718 }
719 return getGitDir().getParentFile();
720 }
721
722 if (getGitDir().getName().equals(DOT_GIT)) {
723 // No value for the "bare" flag, but gitDir is named ".git",
724 // use the parent of the directory
725 //
726 return getGitDir().getParentFile();
727 }
728
729 // We have to assume we are bare.
730 //
731 setBare();
732 return null;
733 }
734
735 /**
736 * Get the configured FS, or {@link FS#DETECTED}.
737 *
738 * @return the configured FS, or {@link FS#DETECTED}.
739 */
740 protected FS safeFS() {
741 return getFS() != null ? getFS() : FS.DETECTED;
742 }
743
744 /**
745 * Get this object
746 *
747 * @return {@code this}
748 */
749 @SuppressWarnings("unchecked")
750 protected final B self() {
751 return (B) this;
752 }
753 }