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 Runtime.getRuntime().addShutdownHook(cleanupHook);
177 try {
178 fetchResult = fetch(repository, u);
179 } catch (IOException ioe) {
180 if (repository != null) {
181 repository.close();
182 }
183 cleanup();
184 throw new JGitInternalException(ioe.getMessage(), ioe);
185 } catch (URISyntaxException e) {
186 if (repository != null) {
187 repository.close();
188 }
189 cleanup();
190 throw new InvalidRemoteException(
191 MessageFormat.format(JGitText.get().invalidRemote, remote),
192 e);
193 } catch (GitAPIException | RuntimeException e) {
194 if (repository != null) {
195 repository.close();
196 }
197 cleanup();
198 throw e;
199 } finally {
200 Runtime.getRuntime().removeShutdownHook(cleanupHook);
201 }
202 if (!noCheckout) {
203 try {
204 checkout(repository, fetchResult);
205 } catch (IOException ioe) {
206 repository.close();
207 throw new JGitInternalException(ioe.getMessage(), ioe);
208 } catch (GitAPIException | RuntimeException e) {
209 repository.close();
210 throw e;
211 }
212 }
213 return new Git(repository, true);
214 }
215
216 private void setFetchType() {
217 if (mirror) {
218 fetchType = FETCH_TYPE.MIRROR;
219 setBare(true);
220 } else if (cloneAllBranches) {
221 fetchType = FETCH_TYPE.ALL_BRANCHES;
222 } else if (branchesToClone != null && !branchesToClone.isEmpty()) {
223 fetchType = FETCH_TYPE.MULTIPLE_BRANCHES;
224 } else {
225
226 fetchType = FETCH_TYPE.ALL_BRANCHES;
227 }
228 }
229
230 private static boolean isNonEmptyDirectory(File dir) {
231 if (dir != null && dir.exists()) {
232 File[] files = dir.listFiles();
233 return files != null && files.length != 0;
234 }
235 return false;
236 }
237
238 void verifyDirectories(URIish u) {
239 if (directory == null && gitDir == null) {
240 directory = new File(u.getHumanishName() + (bare ? Constants.DOT_GIT_EXT : ""));
241 }
242 directoryExistsInitially = directory != null && directory.exists();
243 gitDirExistsInitially = gitDir != null && gitDir.exists();
244 validateDirs(directory, gitDir, bare);
245 if (isNonEmptyDirectory(directory)) {
246 throw new JGitInternalException(MessageFormat.format(
247 JGitText.get().cloneNonEmptyDirectory, directory.getName()));
248 }
249 if (isNonEmptyDirectory(gitDir)) {
250 throw new JGitInternalException(MessageFormat.format(
251 JGitText.get().cloneNonEmptyDirectory, gitDir.getName()));
252 }
253 }
254
255 private Repository init() throws GitAPIException {
256 InitCommand command = Git.init();
257 command.setBare(bare);
258 if (fs != null) {
259 command.setFs(fs);
260 }
261 if (directory != null) {
262 command.setDirectory(directory);
263 }
264 if (gitDir != null) {
265 command.setGitDir(gitDir);
266 }
267 return command.call().getRepository();
268 }
269
270 private FetchResult fetch(Repository clonedRepo, URIish u)
271 throws URISyntaxException,
272 org.eclipse.jgit.api.errors.TransportException, IOException,
273 GitAPIException {
274
275 RemoteConfig config = new RemoteConfig(clonedRepo.getConfig(), remote);
276 config.addURI(u);
277
278 boolean fetchAll = fetchType == FETCH_TYPE.ALL_BRANCHES
279 || fetchType == FETCH_TYPE.MIRROR;
280
281 config.setFetchRefSpecs(calculateRefSpecs(fetchType, config.getName()));
282 config.setMirror(fetchType == FETCH_TYPE.MIRROR);
283 if (tagOption != null) {
284 config.setTagOpt(tagOption);
285 }
286 config.update(clonedRepo.getConfig());
287
288 clonedRepo.getConfig().save();
289
290
291 FetchCommand command = new FetchCommand(clonedRepo);
292 command.setRemote(remote);
293 command.setProgressMonitor(monitor);
294 if (tagOption != null) {
295 command.setTagOpt(tagOption);
296 } else {
297 command.setTagOpt(
298 fetchAll ? TagOpt.FETCH_TAGS : TagOpt.AUTO_FOLLOW);
299 }
300 configure(command);
301
302 return command.call();
303 }
304
305 private List<RefSpec> calculateRefSpecs(FETCH_TYPE type,
306 String remoteName) {
307 List<RefSpec> specs = new ArrayList<>();
308 if (type == FETCH_TYPE.MIRROR) {
309 specs.add(new RefSpec().setForceUpdate(true).setSourceDestination(
310 Constants.R_REFS + '*', Constants.R_REFS + '*'));
311 } else {
312 RefSpec heads = new RefSpec();
313 heads = heads.setForceUpdate(true);
314 final String dst = (bare ? Constants.R_HEADS
315 : Constants.R_REMOTES + remoteName + '/') + '*';
316 heads = heads.setSourceDestination(Constants.R_HEADS + '*', dst);
317 if (type == FETCH_TYPE.MULTIPLE_BRANCHES) {
318 RefSpec tags = new RefSpec().setForceUpdate(true)
319 .setSourceDestination(Constants.R_TAGS + '*',
320 Constants.R_TAGS + '*');
321 for (String selectedRef : branchesToClone) {
322 if (heads.matchSource(selectedRef)) {
323 specs.add(heads.expandFromSource(selectedRef));
324 } else if (tags.matchSource(selectedRef)) {
325 specs.add(tags.expandFromSource(selectedRef));
326 }
327 }
328 } else {
329
330 specs.add(heads);
331 }
332 }
333 return specs;
334 }
335
336 private void checkout(Repository clonedRepo, FetchResult result)
337 throws MissingObjectException, IncorrectObjectTypeException,
338 IOException, GitAPIException {
339
340 Ref head = null;
341 if (branch.equals(Constants.HEAD)) {
342 Ref foundBranch = findBranchToCheckout(result);
343 if (foundBranch != null)
344 head = foundBranch;
345 }
346 if (head == null) {
347 head = result.getAdvertisedRef(branch);
348 if (head == null)
349 head = result.getAdvertisedRef(Constants.R_HEADS + branch);
350 if (head == null)
351 head = result.getAdvertisedRef(Constants.R_TAGS + branch);
352 }
353
354 if (head == null || head.getObjectId() == null)
355 return;
356
357 if (head.getName().startsWith(Constants.R_HEADS)) {
358 final RefUpdate newHead = clonedRepo.updateRef(Constants.HEAD);
359 newHead.disableRefLog();
360 newHead.link(head.getName());
361 addMergeConfig(clonedRepo, head);
362 }
363
364 final RevCommit commit = parseCommit(clonedRepo, head);
365
366 boolean detached = !head.getName().startsWith(Constants.R_HEADS);
367 RefUpdate u = clonedRepo.updateRef(Constants.HEAD, detached);
368 u.setNewObjectId(commit.getId());
369 u.forceUpdate();
370
371 if (!bare) {
372 DirCache dc = clonedRepo.lockDirCache();
373 DirCacheCheckout co = new DirCacheCheckout(clonedRepo, dc,
374 commit.getTree());
375 co.setProgressMonitor(monitor);
376 co.checkout();
377 if (cloneSubmodules)
378 cloneSubmodules(clonedRepo);
379 }
380 }
381
382 private void cloneSubmodules(Repository clonedRepo) throws IOException,
383 GitAPIException {
384 SubmoduleInitCommand init = new SubmoduleInitCommand(clonedRepo);
385 Collection<String> submodules = init.call();
386 if (submodules.isEmpty()) {
387 return;
388 }
389 if (callback != null) {
390 callback.initializedSubmodules(submodules);
391 }
392
393 SubmoduleUpdateCommand update = new SubmoduleUpdateCommand(clonedRepo);
394 configure(update);
395 update.setProgressMonitor(monitor);
396 update.setCallback(callback);
397 if (!update.call().isEmpty()) {
398 SubmoduleWalk walk = SubmoduleWalk.forIndex(clonedRepo);
399 while (walk.next()) {
400 try (Repository subRepo = walk.getRepository()) {
401 if (subRepo != null) {
402 cloneSubmodules(subRepo);
403 }
404 }
405 }
406 }
407 }
408
409 private Ref findBranchToCheckout(FetchResult result) {
410 final Ref idHEAD = result.getAdvertisedRef(Constants.HEAD);
411 ObjectId headId = idHEAD != null ? idHEAD.getObjectId() : null;
412 if (headId == null) {
413 return null;
414 }
415
416 Ref master = result.getAdvertisedRef(Constants.R_HEADS
417 + Constants.MASTER);
418 ObjectId objectId = master != null ? master.getObjectId() : null;
419 if (headId.equals(objectId)) {
420 return master;
421 }
422
423 Ref foundBranch = null;
424 for (Ref r : result.getAdvertisedRefs()) {
425 final String n = r.getName();
426 if (!n.startsWith(Constants.R_HEADS))
427 continue;
428 if (headId.equals(r.getObjectId())) {
429 foundBranch = r;
430 break;
431 }
432 }
433 return foundBranch;
434 }
435
436 private void addMergeConfig(Repository clonedRepo, Ref head)
437 throws IOException {
438 String branchName = Repository.shortenRefName(head.getName());
439 clonedRepo.getConfig().setString(ConfigConstants.CONFIG_BRANCH_SECTION,
440 branchName, ConfigConstants.CONFIG_KEY_REMOTE, remote);
441 clonedRepo.getConfig().setString(ConfigConstants.CONFIG_BRANCH_SECTION,
442 branchName, ConfigConstants.CONFIG_KEY_MERGE, head.getName());
443 String autosetupRebase = clonedRepo.getConfig().getString(
444 ConfigConstants.CONFIG_BRANCH_SECTION, null,
445 ConfigConstants.CONFIG_KEY_AUTOSETUPREBASE);
446 if (ConfigConstants.CONFIG_KEY_ALWAYS.equals(autosetupRebase)
447 || ConfigConstants.CONFIG_KEY_REMOTE.equals(autosetupRebase))
448 clonedRepo.getConfig().setEnum(
449 ConfigConstants.CONFIG_BRANCH_SECTION, branchName,
450 ConfigConstants.CONFIG_KEY_REBASE, BranchRebaseMode.REBASE);
451 clonedRepo.getConfig().save();
452 }
453
454 private RevCommit parseCommit(Repository clonedRepo, Ref ref)
455 throws MissingObjectException, IncorrectObjectTypeException,
456 IOException {
457 final RevCommit commit;
458 try (RevWalkRevWalk.html#RevWalk">RevWalk rw = new RevWalk(clonedRepo)) {
459 commit = rw.parseCommit(ref.getObjectId());
460 }
461 return commit;
462 }
463
464
465
466
467
468
469
470
471
472 public CloneCommand setURI(String uri) {
473 this.uri = uri;
474 return this;
475 }
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492 public CloneCommand setDirectory(File directory) {
493 validateDirs(directory, gitDir, bare);
494 this.directory = directory;
495 return this;
496 }
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512 public CloneCommand setGitDir(File gitDir) {
513 validateDirs(directory, gitDir, bare);
514 this.gitDir = gitDir;
515 return this;
516 }
517
518
519
520
521
522
523
524
525
526
527
528
529
530 public CloneCommand setBare(boolean bare) throws IllegalStateException {
531 validateDirs(directory, gitDir, bare);
532 this.bare = bare;
533 return this;
534 }
535
536
537
538
539
540
541
542
543
544
545 public CloneCommand setFs(FS fs) {
546 this.fs = fs;
547 return this;
548 }
549
550
551
552
553
554
555
556
557
558
559
560
561 public CloneCommand setRemote(String remote) {
562 if (remote == null) {
563 remote = Constants.DEFAULT_REMOTE_NAME;
564 }
565 this.remote = remote;
566 return this;
567 }
568
569
570
571
572
573
574
575
576
577
578
579
580
581 public CloneCommand setBranch(String branch) {
582 if (branch == null) {
583 branch = Constants.HEAD;
584 }
585 this.branch = branch;
586 return this;
587 }
588
589
590
591
592
593
594
595
596
597
598 public CloneCommand setProgressMonitor(ProgressMonitor monitor) {
599 if (monitor == null) {
600 monitor = NullProgressMonitor.INSTANCE;
601 }
602 this.monitor = monitor;
603 return this;
604 }
605
606
607
608
609
610
611
612
613
614
615
616
617
618 public CloneCommand setCloneAllBranches(boolean cloneAllBranches) {
619 this.cloneAllBranches = cloneAllBranches;
620 return this;
621 }
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638 public CloneCommand setMirror(boolean mirror) {
639 this.mirror = mirror;
640 return this;
641 }
642
643
644
645
646
647
648
649
650
651 public CloneCommand setCloneSubmodules(boolean cloneSubmodules) {
652 this.cloneSubmodules = cloneSubmodules;
653 return this;
654 }
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671 public CloneCommand setBranchesToClone(Collection<String> branchesToClone) {
672 this.branchesToClone = branchesToClone;
673 return this;
674 }
675
676
677
678
679
680
681
682
683
684 public CloneCommand setTagOption(TagOpt tagOption) {
685 this.tagOption = tagOption;
686 return this;
687 }
688
689
690
691
692
693
694
695
696 public CloneCommand setNoTags() {
697 return setTagOption(TagOpt.NO_TAGS);
698 }
699
700
701
702
703
704
705
706
707
708
709 public CloneCommand setNoCheckout(boolean noCheckout) {
710 this.noCheckout = noCheckout;
711 return this;
712 }
713
714
715
716
717
718
719
720
721
722 public CloneCommand setCallback(Callback callback) {
723 this.callback = callback;
724 return this;
725 }
726
727 private static void validateDirs(File directory, File gitDir, boolean bare)
728 throws IllegalStateException {
729 if (directory != null) {
730 if (directory.exists() && !directory.isDirectory()) {
731 throw new IllegalStateException(MessageFormat.format(
732 JGitText.get().initFailedDirIsNoDirectory, directory));
733 }
734 if (gitDir != null && gitDir.exists() && !gitDir.isDirectory()) {
735 throw new IllegalStateException(MessageFormat.format(
736 JGitText.get().initFailedGitDirIsNoDirectory,
737 gitDir));
738 }
739 if (bare) {
740 if (gitDir != null && !gitDir.equals(directory))
741 throw new IllegalStateException(MessageFormat.format(
742 JGitText.get().initFailedBareRepoDifferentDirs,
743 gitDir, directory));
744 } else {
745 if (gitDir != null && gitDir.equals(directory))
746 throw new IllegalStateException(MessageFormat.format(
747 JGitText.get().initFailedNonBareRepoSameDirs,
748 gitDir, directory));
749 }
750 }
751 }
752
753 private void cleanup() {
754 try {
755 if (directory != null) {
756 if (!directoryExistsInitially) {
757 FileUtils.delete(directory, FileUtils.RECURSIVE
758 | FileUtils.SKIP_MISSING | FileUtils.IGNORE_ERRORS);
759 } else {
760 deleteChildren(directory);
761 }
762 }
763 if (gitDir != null) {
764 if (!gitDirExistsInitially) {
765 FileUtils.delete(gitDir, FileUtils.RECURSIVE
766 | FileUtils.SKIP_MISSING | FileUtils.IGNORE_ERRORS);
767 } else {
768 deleteChildren(gitDir);
769 }
770 }
771 } catch (IOException e) {
772
773
774 }
775 }
776
777 private void deleteChildren(File file) throws IOException {
778 File[] files = file.listFiles();
779 if (files == null) {
780 return;
781 }
782 for (File child : files) {
783 FileUtils.delete(child, FileUtils.RECURSIVE | FileUtils.SKIP_MISSING
784 | FileUtils.IGNORE_ERRORS);
785 }
786 }
787 }