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