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