1
2
3
4
5
6
7
8
9
10 package org.eclipse.jgit.api;
11
12 import java.io.File;
13 import java.io.IOException;
14 import java.net.URISyntaxException;
15 import java.text.MessageFormat;
16 import java.util.ArrayList;
17 import java.util.Collection;
18 import java.util.List;
19
20 import org.eclipse.jgit.annotations.Nullable;
21 import org.eclipse.jgit.api.errors.GitAPIException;
22 import org.eclipse.jgit.api.errors.InvalidRemoteException;
23 import org.eclipse.jgit.api.errors.JGitInternalException;
24 import org.eclipse.jgit.dircache.DirCache;
25 import org.eclipse.jgit.dircache.DirCacheCheckout;
26 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
27 import org.eclipse.jgit.errors.MissingObjectException;
28 import org.eclipse.jgit.internal.JGitText;
29 import org.eclipse.jgit.lib.AnyObjectId;
30 import org.eclipse.jgit.lib.BranchConfig.BranchRebaseMode;
31 import org.eclipse.jgit.lib.ConfigConstants;
32 import org.eclipse.jgit.lib.Constants;
33 import org.eclipse.jgit.lib.NullProgressMonitor;
34 import org.eclipse.jgit.lib.ObjectId;
35 import org.eclipse.jgit.lib.ProgressMonitor;
36 import org.eclipse.jgit.lib.Ref;
37 import org.eclipse.jgit.lib.RefUpdate;
38 import org.eclipse.jgit.lib.Repository;
39 import org.eclipse.jgit.revwalk.RevCommit;
40 import org.eclipse.jgit.revwalk.RevWalk;
41 import org.eclipse.jgit.submodule.SubmoduleWalk;
42 import org.eclipse.jgit.transport.FetchResult;
43 import org.eclipse.jgit.transport.RefSpec;
44 import org.eclipse.jgit.transport.RemoteConfig;
45 import org.eclipse.jgit.transport.TagOpt;
46 import org.eclipse.jgit.transport.URIish;
47 import org.eclipse.jgit.util.FS;
48 import org.eclipse.jgit.util.FileUtils;
49
50
51
52
53
54
55
56 public class CloneCommand extends TransportCommand<CloneCommand, Git> {
57
58 private String uri;
59
60 private File directory;
61
62 private File gitDir;
63
64 private boolean bare;
65
66 private FS fs;
67
68 private String remote = Constants.DEFAULT_REMOTE_NAME;
69
70 private String branch = Constants.HEAD;
71
72 private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
73
74 private boolean cloneAllBranches;
75
76 private boolean mirror;
77
78 private boolean cloneSubmodules;
79
80 private boolean noCheckout;
81
82 private Collection<String> branchesToClone;
83
84 private Callback callback;
85
86 private boolean directoryExistsInitially;
87
88 private boolean gitDirExistsInitially;
89
90 private FETCH_TYPE fetchType;
91
92 private TagOpt tagOption;
93
94 private enum FETCH_TYPE {
95 MULTIPLE_BRANCHES, ALL_BRANCHES, MIRROR
96 }
97
98
99
100
101
102
103 public interface Callback {
104
105
106
107
108
109
110
111 void initializedSubmodules(Collection<String> submodules);
112
113
114
115
116
117
118
119 void cloningSubmodule(String path);
120
121
122
123
124
125
126
127
128
129 void checkingOut(AnyObjectId commit, String path);
130 }
131
132
133
134
135 public CloneCommand() {
136 super(null);
137 }
138
139
140
141
142
143
144 @Nullable
145 File getDirectory() {
146 return directory;
147 }
148
149
150
151
152
153
154
155
156
157
158
159
160 @Override
161 public Git call() throws GitAPIException, InvalidRemoteException,
162 org.eclipse.jgit.api.errors.TransportException {
163 URIish u = null;
164 try {
165 u = new URIish(uri);
166 verifyDirectories(u);
167 } catch (URISyntaxException e) {
168 throw new InvalidRemoteException(
169 MessageFormat.format(JGitText.get().invalidURL, uri), e);
170 }
171 setFetchType();
172 @SuppressWarnings("resource")
173 Repository repository = init();
174 FetchResult fetchResult = null;
175 Thread cleanupHook = new Thread(() -> cleanup());
176 try {
177 Runtime.getRuntime().addShutdownHook(cleanupHook);
178 } catch (IllegalStateException e) {
179
180 }
181 try {
182 fetchResult = fetch(repository, u);
183 } catch (IOException ioe) {
184 if (repository != null) {
185 repository.close();
186 }
187 cleanup();
188 throw new JGitInternalException(ioe.getMessage(), ioe);
189 } catch (URISyntaxException e) {
190 if (repository != null) {
191 repository.close();
192 }
193 cleanup();
194 throw new InvalidRemoteException(
195 MessageFormat.format(JGitText.get().invalidRemote, remote),
196 e);
197 } catch (GitAPIException | RuntimeException e) {
198 if (repository != null) {
199 repository.close();
200 }
201 cleanup();
202 throw e;
203 } finally {
204 try {
205 Runtime.getRuntime().removeShutdownHook(cleanupHook);
206 } catch (IllegalStateException e) {
207
208 }
209 }
210 if (!noCheckout) {
211 try {
212 checkout(repository, fetchResult);
213 } catch (IOException ioe) {
214 repository.close();
215 throw new JGitInternalException(ioe.getMessage(), ioe);
216 } catch (GitAPIException | RuntimeException e) {
217 repository.close();
218 throw e;
219 }
220 }
221 return new Git(repository, true);
222 }
223
224 private void setFetchType() {
225 if (mirror) {
226 fetchType = FETCH_TYPE.MIRROR;
227 setBare(true);
228 } else if (cloneAllBranches) {
229 fetchType = FETCH_TYPE.ALL_BRANCHES;
230 } else if (branchesToClone != null && !branchesToClone.isEmpty()) {
231 fetchType = FETCH_TYPE.MULTIPLE_BRANCHES;
232 } else {
233
234 fetchType = FETCH_TYPE.ALL_BRANCHES;
235 }
236 }
237
238 private static boolean isNonEmptyDirectory(File dir) {
239 if (dir != null && dir.exists()) {
240 File[] files = dir.listFiles();
241 return files != null && files.length != 0;
242 }
243 return false;
244 }
245
246 void verifyDirectories(URIish u) {
247 if (directory == null && gitDir == null) {
248 directory = new File(u.getHumanishName() + (bare ? Constants.DOT_GIT_EXT : ""));
249 }
250 directoryExistsInitially = directory != null && directory.exists();
251 gitDirExistsInitially = gitDir != null && gitDir.exists();
252 validateDirs(directory, gitDir, bare);
253 if (isNonEmptyDirectory(directory)) {
254 throw new JGitInternalException(MessageFormat.format(
255 JGitText.get().cloneNonEmptyDirectory, directory.getName()));
256 }
257 if (isNonEmptyDirectory(gitDir)) {
258 throw new JGitInternalException(MessageFormat.format(
259 JGitText.get().cloneNonEmptyDirectory, gitDir.getName()));
260 }
261 }
262
263 private Repository init() throws GitAPIException {
264 InitCommand command = Git.init();
265 command.setBare(bare);
266 if (fs != null) {
267 command.setFs(fs);
268 }
269 if (directory != null) {
270 command.setDirectory(directory);
271 }
272 if (gitDir != null) {
273 command.setGitDir(gitDir);
274 }
275 return command.call().getRepository();
276 }
277
278 private FetchResult fetch(Repository clonedRepo, URIish u)
279 throws URISyntaxException,
280 org.eclipse.jgit.api.errors.TransportException, IOException,
281 GitAPIException {
282
283 RemoteConfig config = new RemoteConfig(clonedRepo.getConfig(), remote);
284 config.addURI(u);
285
286 boolean fetchAll = fetchType == FETCH_TYPE.ALL_BRANCHES
287 || fetchType == FETCH_TYPE.MIRROR;
288
289 config.setFetchRefSpecs(calculateRefSpecs(fetchType, config.getName()));
290 config.setMirror(fetchType == FETCH_TYPE.MIRROR);
291 if (tagOption != null) {
292 config.setTagOpt(tagOption);
293 }
294 config.update(clonedRepo.getConfig());
295
296 clonedRepo.getConfig().save();
297
298
299 FetchCommand command = new FetchCommand(clonedRepo);
300 command.setRemote(remote);
301 command.setProgressMonitor(monitor);
302 if (tagOption != null) {
303 command.setTagOpt(tagOption);
304 } else {
305 command.setTagOpt(
306 fetchAll ? TagOpt.FETCH_TAGS : TagOpt.AUTO_FOLLOW);
307 }
308 command.setInitialBranch(branch);
309 configure(command);
310
311 return command.call();
312 }
313
314 private List<RefSpec> calculateRefSpecs(FETCH_TYPE type,
315 String remoteName) {
316 List<RefSpec> specs = new ArrayList<>();
317 if (type == FETCH_TYPE.MIRROR) {
318 specs.add(new RefSpec().setForceUpdate(true).setSourceDestination(
319 Constants.R_REFS + '*', Constants.R_REFS + '*'));
320 } else {
321 RefSpec heads = new RefSpec();
322 heads = heads.setForceUpdate(true);
323 final String dst = (bare ? Constants.R_HEADS
324 : Constants.R_REMOTES + remoteName + '/') + '*';
325 heads = heads.setSourceDestination(Constants.R_HEADS + '*', dst);
326 if (type == FETCH_TYPE.MULTIPLE_BRANCHES) {
327 RefSpec tags = new RefSpec().setForceUpdate(true)
328 .setSourceDestination(Constants.R_TAGS + '*',
329 Constants.R_TAGS + '*');
330 for (String selectedRef : branchesToClone) {
331 if (heads.matchSource(selectedRef)) {
332 specs.add(heads.expandFromSource(selectedRef));
333 } else if (tags.matchSource(selectedRef)) {
334 specs.add(tags.expandFromSource(selectedRef));
335 }
336 }
337 } else {
338
339 specs.add(heads);
340 }
341 }
342 return specs;
343 }
344
345 private void checkout(Repository clonedRepo, FetchResult result)
346 throws MissingObjectException, IncorrectObjectTypeException,
347 IOException, GitAPIException {
348
349 Ref head = null;
350 if (branch.equals(Constants.HEAD)) {
351 Ref foundBranch = findBranchToCheckout(result);
352 if (foundBranch != null)
353 head = foundBranch;
354 }
355 if (head == null) {
356 head = result.getAdvertisedRef(branch);
357 if (head == null)
358 head = result.getAdvertisedRef(Constants.R_HEADS + branch);
359 if (head == null)
360 head = result.getAdvertisedRef(Constants.R_TAGS + branch);
361 }
362
363 if (head == null || head.getObjectId() == null)
364 return;
365
366 if (head.getName().startsWith(Constants.R_HEADS)) {
367 final RefUpdate newHead = clonedRepo.updateRef(Constants.HEAD);
368 newHead.disableRefLog();
369 newHead.link(head.getName());
370 addMergeConfig(clonedRepo, head);
371 }
372
373 final RevCommit commit = parseCommit(clonedRepo, head);
374
375 boolean detached = !head.getName().startsWith(Constants.R_HEADS);
376 RefUpdate u = clonedRepo.updateRef(Constants.HEAD, detached);
377 u.setNewObjectId(commit.getId());
378 u.forceUpdate();
379
380 if (!bare) {
381 DirCache dc = clonedRepo.lockDirCache();
382 DirCacheCheckout co = new DirCacheCheckout(clonedRepo, dc,
383 commit.getTree());
384 co.setProgressMonitor(monitor);
385 co.checkout();
386 if (cloneSubmodules)
387 cloneSubmodules(clonedRepo);
388 }
389 }
390
391 private void cloneSubmodules(Repository clonedRepo) throws IOException,
392 GitAPIException {
393 SubmoduleInitCommand init = new SubmoduleInitCommand(clonedRepo);
394 Collection<String> submodules = init.call();
395 if (submodules.isEmpty()) {
396 return;
397 }
398 if (callback != null) {
399 callback.initializedSubmodules(submodules);
400 }
401
402 SubmoduleUpdateCommand update = new SubmoduleUpdateCommand(clonedRepo);
403 configure(update);
404 update.setProgressMonitor(monitor);
405 update.setCallback(callback);
406 if (!update.call().isEmpty()) {
407 SubmoduleWalk walk = SubmoduleWalk.forIndex(clonedRepo);
408 while (walk.next()) {
409 try (Repository subRepo = walk.getRepository()) {
410 if (subRepo != null) {
411 cloneSubmodules(subRepo);
412 }
413 }
414 }
415 }
416 }
417
418 private Ref findBranchToCheckout(FetchResult result) {
419 final Ref idHEAD = result.getAdvertisedRef(Constants.HEAD);
420 ObjectId headId = idHEAD != null ? idHEAD.getObjectId() : null;
421 if (headId == null) {
422 return null;
423 }
424
425 if (idHEAD != null && idHEAD.isSymbolic()) {
426 return idHEAD.getTarget();
427 }
428
429 Ref master = result.getAdvertisedRef(Constants.R_HEADS
430 + Constants.MASTER);
431 ObjectId objectId = master != null ? master.getObjectId() : null;
432 if (headId.equals(objectId)) {
433 return master;
434 }
435
436 Ref foundBranch = null;
437 for (Ref r : result.getAdvertisedRefs()) {
438 final String n = r.getName();
439 if (!n.startsWith(Constants.R_HEADS))
440 continue;
441 if (headId.equals(r.getObjectId())) {
442 foundBranch = r;
443 break;
444 }
445 }
446 return foundBranch;
447 }
448
449 private void addMergeConfig(Repository clonedRepo, Ref head)
450 throws IOException {
451 String branchName = Repository.shortenRefName(head.getName());
452 clonedRepo.getConfig().setString(ConfigConstants.CONFIG_BRANCH_SECTION,
453 branchName, ConfigConstants.CONFIG_KEY_REMOTE, remote);
454 clonedRepo.getConfig().setString(ConfigConstants.CONFIG_BRANCH_SECTION,
455 branchName, ConfigConstants.CONFIG_KEY_MERGE, head.getName());
456 String autosetupRebase = clonedRepo.getConfig().getString(
457 ConfigConstants.CONFIG_BRANCH_SECTION, null,
458 ConfigConstants.CONFIG_KEY_AUTOSETUPREBASE);
459 if (ConfigConstants.CONFIG_KEY_ALWAYS.equals(autosetupRebase)
460 || ConfigConstants.CONFIG_KEY_REMOTE.equals(autosetupRebase))
461 clonedRepo.getConfig().setEnum(
462 ConfigConstants.CONFIG_BRANCH_SECTION, branchName,
463 ConfigConstants.CONFIG_KEY_REBASE, BranchRebaseMode.REBASE);
464 clonedRepo.getConfig().save();
465 }
466
467 private RevCommit parseCommit(Repository clonedRepo, Ref ref)
468 throws MissingObjectException, IncorrectObjectTypeException,
469 IOException {
470 final RevCommit commit;
471 try (RevWalk rw = new RevWalk(clonedRepo)) {
472 commit = rw.parseCommit(ref.getObjectId());
473 }
474 return commit;
475 }
476
477
478
479
480
481
482
483
484
485 public CloneCommand setURI(String uri) {
486 this.uri = uri;
487 return this;
488 }
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505 public CloneCommand setDirectory(File directory) {
506 validateDirs(directory, gitDir, bare);
507 this.directory = directory;
508 return this;
509 }
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525 public CloneCommand setGitDir(File gitDir) {
526 validateDirs(directory, gitDir, bare);
527 this.gitDir = gitDir;
528 return this;
529 }
530
531
532
533
534
535
536
537
538
539
540
541
542
543 public CloneCommand setBare(boolean bare) throws IllegalStateException {
544 validateDirs(directory, gitDir, bare);
545 this.bare = bare;
546 return this;
547 }
548
549
550
551
552
553
554
555
556
557
558 public CloneCommand setFs(FS fs) {
559 this.fs = fs;
560 return this;
561 }
562
563
564
565
566
567
568
569
570
571
572
573
574 public CloneCommand setRemote(String remote) {
575 if (remote == null) {
576 remote = Constants.DEFAULT_REMOTE_NAME;
577 }
578 this.remote = remote;
579 return this;
580 }
581
582
583
584
585
586
587
588
589
590
591
592
593
594 public CloneCommand setBranch(String branch) {
595 if (branch == null) {
596 branch = Constants.HEAD;
597 }
598 this.branch = branch;
599 return this;
600 }
601
602
603
604
605
606
607
608
609
610
611 public CloneCommand setProgressMonitor(ProgressMonitor monitor) {
612 if (monitor == null) {
613 monitor = NullProgressMonitor.INSTANCE;
614 }
615 this.monitor = monitor;
616 return this;
617 }
618
619
620
621
622
623
624
625
626
627
628
629
630
631 public CloneCommand setCloneAllBranches(boolean cloneAllBranches) {
632 this.cloneAllBranches = cloneAllBranches;
633 return this;
634 }
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651 public CloneCommand setMirror(boolean mirror) {
652 this.mirror = mirror;
653 return this;
654 }
655
656
657
658
659
660
661
662
663
664 public CloneCommand setCloneSubmodules(boolean cloneSubmodules) {
665 this.cloneSubmodules = cloneSubmodules;
666 return this;
667 }
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684 public CloneCommand setBranchesToClone(Collection<String> branchesToClone) {
685 this.branchesToClone = branchesToClone;
686 return this;
687 }
688
689
690
691
692
693
694
695
696
697 public CloneCommand setTagOption(TagOpt tagOption) {
698 this.tagOption = tagOption;
699 return this;
700 }
701
702
703
704
705
706
707
708
709 public CloneCommand setNoTags() {
710 return setTagOption(TagOpt.NO_TAGS);
711 }
712
713
714
715
716
717
718
719
720
721
722 public CloneCommand setNoCheckout(boolean noCheckout) {
723 this.noCheckout = noCheckout;
724 return this;
725 }
726
727
728
729
730
731
732
733
734
735 public CloneCommand setCallback(Callback callback) {
736 this.callback = callback;
737 return this;
738 }
739
740 private static void validateDirs(File directory, File gitDir, boolean bare)
741 throws IllegalStateException {
742 if (directory != null) {
743 if (directory.exists() && !directory.isDirectory()) {
744 throw new IllegalStateException(MessageFormat.format(
745 JGitText.get().initFailedDirIsNoDirectory, directory));
746 }
747 if (gitDir != null && gitDir.exists() && !gitDir.isDirectory()) {
748 throw new IllegalStateException(MessageFormat.format(
749 JGitText.get().initFailedGitDirIsNoDirectory,
750 gitDir));
751 }
752 if (bare) {
753 if (gitDir != null && !gitDir.equals(directory))
754 throw new IllegalStateException(MessageFormat.format(
755 JGitText.get().initFailedBareRepoDifferentDirs,
756 gitDir, directory));
757 } else {
758 if (gitDir != null && gitDir.equals(directory))
759 throw new IllegalStateException(MessageFormat.format(
760 JGitText.get().initFailedNonBareRepoSameDirs,
761 gitDir, directory));
762 }
763 }
764 }
765
766 private void cleanup() {
767 try {
768 if (directory != null) {
769 if (!directoryExistsInitially) {
770 FileUtils.delete(directory, FileUtils.RECURSIVE
771 | FileUtils.SKIP_MISSING | FileUtils.IGNORE_ERRORS);
772 } else {
773 deleteChildren(directory);
774 }
775 }
776 if (gitDir != null) {
777 if (!gitDirExistsInitially) {
778 FileUtils.delete(gitDir, FileUtils.RECURSIVE
779 | FileUtils.SKIP_MISSING | FileUtils.IGNORE_ERRORS);
780 } else {
781 deleteChildren(gitDir);
782 }
783 }
784 } catch (IOException e) {
785
786
787 }
788 }
789
790 private void deleteChildren(File file) throws IOException {
791 File[] files = file.listFiles();
792 if (files == null) {
793 return;
794 }
795 for (File child : files) {
796 FileUtils.delete(child, FileUtils.RECURSIVE | FileUtils.SKIP_MISSING
797 | FileUtils.IGNORE_ERRORS);
798 }
799 }
800 }