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.gitrepo;
44
45 import static java.nio.charset.StandardCharsets.UTF_8;
46 import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME;
47 import static org.eclipse.jgit.lib.Constants.R_REMOTES;
48
49 import java.io.File;
50 import java.io.FileInputStream;
51 import java.io.IOException;
52 import java.io.InputStream;
53 import java.net.URI;
54 import java.text.MessageFormat;
55 import java.util.ArrayList;
56 import java.util.List;
57 import java.util.Map;
58 import java.util.Objects;
59 import java.util.StringJoiner;
60 import java.util.TreeMap;
61
62 import org.eclipse.jgit.annotations.NonNull;
63 import org.eclipse.jgit.annotations.Nullable;
64 import org.eclipse.jgit.api.Git;
65 import org.eclipse.jgit.api.GitCommand;
66 import org.eclipse.jgit.api.SubmoduleAddCommand;
67 import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
68 import org.eclipse.jgit.api.errors.GitAPIException;
69 import org.eclipse.jgit.api.errors.InvalidRefNameException;
70 import org.eclipse.jgit.api.errors.JGitInternalException;
71 import org.eclipse.jgit.dircache.DirCache;
72 import org.eclipse.jgit.dircache.DirCacheBuilder;
73 import org.eclipse.jgit.dircache.DirCacheEntry;
74 import org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader;
75 import org.eclipse.jgit.gitrepo.RepoProject.CopyFile;
76 import org.eclipse.jgit.gitrepo.RepoProject.LinkFile;
77 import org.eclipse.jgit.gitrepo.internal.RepoText;
78 import org.eclipse.jgit.internal.JGitText;
79 import org.eclipse.jgit.lib.CommitBuilder;
80 import org.eclipse.jgit.lib.Config;
81 import org.eclipse.jgit.lib.Constants;
82 import org.eclipse.jgit.lib.FileMode;
83 import org.eclipse.jgit.lib.ObjectId;
84 import org.eclipse.jgit.lib.ObjectInserter;
85 import org.eclipse.jgit.lib.PersonIdent;
86 import org.eclipse.jgit.lib.ProgressMonitor;
87 import org.eclipse.jgit.lib.Ref;
88 import org.eclipse.jgit.lib.RefDatabase;
89 import org.eclipse.jgit.lib.RefUpdate;
90 import org.eclipse.jgit.lib.RefUpdate.Result;
91 import org.eclipse.jgit.lib.Repository;
92 import org.eclipse.jgit.revwalk.RevCommit;
93 import org.eclipse.jgit.revwalk.RevWalk;
94 import org.eclipse.jgit.treewalk.TreeWalk;
95 import org.eclipse.jgit.util.FileUtils;
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114 public class RepoCommand extends GitCommand<RevCommit> {
115 private String manifestPath;
116 private String baseUri;
117 private URI targetUri;
118 private String groupsParam;
119 private String branch;
120 private String targetBranch = Constants.HEAD;
121 private boolean recordRemoteBranch = true;
122 private boolean recordSubmoduleLabels = true;
123 private boolean recordShallowSubmodules = true;
124 private PersonIdent author;
125 private RemoteReader callback;
126 private InputStream inputStream;
127 private IncludedFileReader includedReader;
128 private boolean ignoreRemoteFailures = false;
129
130 private ProgressMonitor monitor;
131
132
133
134
135
136
137
138
139
140
141
142 public interface RemoteReader {
143
144
145
146
147
148
149
150
151
152
153
154
155
156 @Nullable
157 public ObjectId sha1(String uri, String ref) throws GitAPIException;
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176 @Deprecated
177 public default byte[] readFile(String uri, String ref, String path)
178 throws GitAPIException, IOException {
179 return readFileWithMode(uri, ref, path).getContents();
180 }
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203 @NonNull
204 public RemoteFile readFileWithMode(String uri, String ref, String path)
205 throws GitAPIException, IOException;
206 }
207
208
209
210
211
212
213
214 public static final class RemoteFile {
215 @NonNull
216 private final byte[] contents;
217
218 @NonNull
219 private final FileMode fileMode;
220
221
222
223
224
225
226
227 public RemoteFile(@NonNull byte[] contents,
228 @NonNull FileMode fileMode) {
229 this.contents = Objects.requireNonNull(contents);
230 this.fileMode = Objects.requireNonNull(fileMode);
231 }
232
233
234
235
236
237
238
239
240
241 @NonNull
242 public byte[] getContents() {
243 return contents;
244 }
245
246
247
248
249 @NonNull
250 public FileMode getFileMode() {
251 return fileMode;
252 }
253
254 }
255
256
257 public static class DefaultRemoteReader implements RemoteReader {
258
259 @Override
260 public ObjectId sha1(String uri, String ref) throws GitAPIException {
261 Map<String, Ref> map = Git
262 .lsRemoteRepository()
263 .setRemote(uri)
264 .callAsMap();
265 Ref r = RefDatabase.findRef(map, ref);
266 return r != null ? r.getObjectId() : null;
267 }
268
269 @Override
270 public RemoteFile readFileWithMode(String uri, String ref, String path)
271 throws GitAPIException, IOException {
272 File dir = FileUtils.createTempDir("jgit_", ".git", null);
273 try (Git git = Git.cloneRepository().setBare(true).setDirectory(dir)
274 .setURI(uri).call()) {
275 Repository repo = git.getRepository();
276 ObjectId refCommitId = sha1(uri, ref);
277 if (refCommitId == null) {
278 throw new InvalidRefNameException(MessageFormat
279 .format(JGitText.get().refNotResolved, ref));
280 }
281 RevCommit commit = repo.parseCommit(refCommitId);
282 TreeWalk tw = TreeWalk.forPath(repo, path, commit.getTree());
283
284
285
286 return new RemoteFile(
287 tw.getObjectReader().open(tw.getObjectId(0))
288 .getCachedBytes(Integer.MAX_VALUE),
289 tw.getFileMode(0));
290 } finally {
291 FileUtils.delete(dir, FileUtils.RECURSIVE);
292 }
293 }
294 }
295
296 @SuppressWarnings("serial")
297 private static class ManifestErrorException extends GitAPIException {
298 ManifestErrorException(Throwable cause) {
299 super(RepoText.get().invalidManifest, cause);
300 }
301 }
302
303 @SuppressWarnings("serial")
304 private static class RemoteUnavailableException extends GitAPIException {
305 RemoteUnavailableException(String uri) {
306 super(MessageFormat.format(RepoText.get().errorRemoteUnavailable, uri));
307 }
308 }
309
310
311
312
313
314
315
316 public RepoCommand(Repository repo) {
317 super(repo);
318 }
319
320
321
322
323
324
325
326
327
328
329 public RepoCommand setPath(String path) {
330 this.manifestPath = path;
331 return this;
332 }
333
334
335
336
337
338
339
340
341
342
343
344 public RepoCommand setInputStream(InputStream inputStream) {
345 this.inputStream = inputStream;
346 return this;
347 }
348
349
350
351
352
353
354
355
356
357
358
359
360 public RepoCommand setURI(String uri) {
361 this.baseUri = uri;
362 return this;
363 }
364
365
366
367
368
369
370
371
372
373
374 public RepoCommand setTargetURI(String uri) {
375
376
377
378
379 this.targetUri = URI.create(uri + "/");
380 return this;
381 }
382
383
384
385
386
387
388
389 public RepoCommand setGroups(String groups) {
390 this.groupsParam = groups;
391 return this;
392 }
393
394
395
396
397
398
399
400
401
402
403
404
405 public RepoCommand setBranch(String branch) {
406 this.branch = branch;
407 return this;
408 }
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424 public RepoCommand setTargetBranch(String branch) {
425 this.targetBranch = Constants.R_HEADS + branch;
426 return this;
427 }
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448 public RepoCommand setRecordRemoteBranch(boolean enable) {
449 this.recordRemoteBranch = enable;
450 return this;
451 }
452
453
454
455
456
457
458
459
460
461
462
463 public RepoCommand setRecordSubmoduleLabels(boolean enable) {
464 this.recordSubmoduleLabels = enable;
465 return this;
466 }
467
468
469
470
471
472
473
474
475
476
477
478 public RepoCommand setRecommendShallow(boolean enable) {
479 this.recordShallowSubmodules = enable;
480 return this;
481 }
482
483
484
485
486
487
488
489
490
491
492 public RepoCommand setProgressMonitor(ProgressMonitor monitor) {
493 this.monitor = monitor;
494 return this;
495 }
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512 public RepoCommand setIgnoreRemoteFailures(boolean ignore) {
513 this.ignoreRemoteFailures = ignore;
514 return this;
515 }
516
517
518
519
520
521
522
523
524
525
526
527 public RepoCommand setAuthor(PersonIdent author) {
528 this.author = author;
529 return this;
530 }
531
532
533
534
535
536
537
538
539
540
541
542 public RepoCommand setRemoteReader(RemoteReader callback) {
543 this.callback = callback;
544 return this;
545 }
546
547
548
549
550
551
552
553
554
555
556
557 public RepoCommand setIncludedFileReader(IncludedFileReader reader) {
558 this.includedReader = reader;
559 return this;
560 }
561
562
563 @Override
564 public RevCommit call() throws GitAPIException {
565 checkCallable();
566 if (baseUri == null) {
567 baseUri = "";
568 }
569 if (inputStream == null) {
570 if (manifestPath == null || manifestPath.length() == 0)
571 throw new IllegalArgumentException(
572 JGitText.get().pathNotConfigured);
573 try {
574 inputStream = new FileInputStream(manifestPath);
575 } catch (IOException e) {
576 throw new IllegalArgumentException(
577 JGitText.get().pathNotConfigured);
578 }
579 }
580
581 List<RepoProject> filteredProjects;
582 try {
583 ManifestParser parser = new ManifestParser(includedReader,
584 manifestPath, branch, baseUri, groupsParam, repo);
585 parser.read(inputStream);
586 filteredProjects = parser.getFilteredProjects();
587 } catch (IOException e) {
588 throw new ManifestErrorException(e);
589 } finally {
590 try {
591 inputStream.close();
592 } catch (IOException e) {
593
594 }
595 }
596
597 if (repo.isBare()) {
598 if (author == null)
599 author = new PersonIdent(repo);
600 if (callback == null)
601 callback = new DefaultRemoteReader();
602 List<RepoProject> renamedProjects = renameProjects(filteredProjects);
603
604 DirCache index = DirCache.newInCore();
605 DirCacheBuilder builder = index.builder();
606 ObjectInserter inserter = repo.newObjectInserter();
607 try (RevWalkRevWalk.html#RevWalk">RevWalk rw = new RevWalk(repo)) {
608 Config cfg = new Config();
609 StringBuilder attributes = new StringBuilder();
610 for (RepoProject proj : renamedProjects) {
611 String name = proj.getName();
612 String path = proj.getPath();
613 String url = proj.getUrl();
614 ObjectId objectId;
615 if (ObjectId.isId(proj.getRevision())) {
616 objectId = ObjectId.fromString(proj.getRevision());
617 } else {
618 objectId = callback.sha1(url, proj.getRevision());
619 if (objectId == null && !ignoreRemoteFailures) {
620 throw new RemoteUnavailableException(url);
621 }
622 if (recordRemoteBranch) {
623
624 cfg.setString("submodule", name, "branch",
625 proj.getRevision());
626 }
627
628 if (recordShallowSubmodules && proj.getRecommendShallow() != null) {
629
630
631
632
633
634 cfg.setBoolean("submodule", name, "shallow",
635 true);
636 }
637 }
638 if (recordSubmoduleLabels) {
639 StringBuilder rec = new StringBuilder();
640 rec.append("/");
641 rec.append(path);
642 for (String group : proj.getGroups()) {
643 rec.append(" ");
644 rec.append(group);
645 }
646 rec.append("\n");
647 attributes.append(rec.toString());
648 }
649
650 URI submodUrl = URI.create(url);
651 if (targetUri != null) {
652 submodUrl = relativize(targetUri, submodUrl);
653 }
654 cfg.setString("submodule", name, "path", path);
655 cfg.setString("submodule", name, "url",
656 submodUrl.toString());
657
658
659 if (objectId != null) {
660 DirCacheEntry dcEntry = new DirCacheEntry(path);
661 dcEntry.setObjectId(objectId);
662 dcEntry.setFileMode(FileMode.GITLINK);
663 builder.add(dcEntry);
664
665 for (CopyFile copyfile : proj.getCopyFiles()) {
666 RemoteFile rf = callback.readFileWithMode(
667 url, proj.getRevision(), copyfile.src);
668 objectId = inserter.insert(Constants.OBJ_BLOB,
669 rf.getContents());
670 dcEntry = new DirCacheEntry(copyfile.dest);
671 dcEntry.setObjectId(objectId);
672 dcEntry.setFileMode(rf.getFileMode());
673 builder.add(dcEntry);
674 }
675 for (LinkFile linkfile : proj.getLinkFiles()) {
676 String link;
677 if (linkfile.dest.contains("/")) {
678 link = FileUtils.relativizeGitPath(
679 linkfile.dest.substring(0,
680 linkfile.dest.lastIndexOf('/')),
681 proj.getPath() + "/" + linkfile.src);
682 } else {
683 link = proj.getPath() + "/" + linkfile.src;
684 }
685
686 objectId = inserter.insert(Constants.OBJ_BLOB,
687 link.getBytes(UTF_8));
688 dcEntry = new DirCacheEntry(linkfile.dest);
689 dcEntry.setObjectId(objectId);
690 dcEntry.setFileMode(FileMode.SYMLINK);
691 builder.add(dcEntry);
692 }
693 }
694 }
695 String content = cfg.toText();
696
697
698 final DirCacheEntrytry.html#DirCacheEntry">DirCacheEntry dcEntry = new DirCacheEntry(Constants.DOT_GIT_MODULES);
699 ObjectId objectId = inserter.insert(Constants.OBJ_BLOB,
700 content.getBytes(UTF_8));
701 dcEntry.setObjectId(objectId);
702 dcEntry.setFileMode(FileMode.REGULAR_FILE);
703 builder.add(dcEntry);
704
705 if (recordSubmoduleLabels) {
706
707 final DirCacheEntryhtml#DirCacheEntry">DirCacheEntry dcEntryAttr = new DirCacheEntry(Constants.DOT_GIT_ATTRIBUTES);
708 ObjectId attrId = inserter.insert(Constants.OBJ_BLOB,
709 attributes.toString().getBytes(UTF_8));
710 dcEntryAttr.setObjectId(attrId);
711 dcEntryAttr.setFileMode(FileMode.REGULAR_FILE);
712 builder.add(dcEntryAttr);
713 }
714
715 builder.finish();
716 ObjectId treeId = index.writeTree(inserter);
717
718
719 ObjectId headId = repo.resolve(targetBranch + "^{commit}");
720 if (headId != null && rw.parseCommit(headId).getTree().getId().equals(treeId)) {
721
722 return rw.parseCommit(headId);
723 }
724
725 CommitBuilder commit = new CommitBuilder();
726 commit.setTreeId(treeId);
727 if (headId != null)
728 commit.setParentIds(headId);
729 commit.setAuthor(author);
730 commit.setCommitter(author);
731 commit.setMessage(RepoText.get().repoCommitMessage);
732
733 ObjectId commitId = inserter.insert(commit);
734 inserter.flush();
735
736 RefUpdate ru = repo.updateRef(targetBranch);
737 ru.setNewObjectId(commitId);
738 ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId());
739 Result rc = ru.update(rw);
740
741 switch (rc) {
742 case NEW:
743 case FORCED:
744 case FAST_FORWARD:
745
746 break;
747 case REJECTED:
748 case LOCK_FAILURE:
749 throw new ConcurrentRefUpdateException(
750 MessageFormat.format(
751 JGitText.get().cannotLock, targetBranch),
752 ru.getRef(),
753 rc);
754 default:
755 throw new JGitInternalException(MessageFormat.format(
756 JGitText.get().updatingRefFailed,
757 targetBranch, commitId.name(), rc));
758 }
759
760 return rw.parseCommit(commitId);
761 } catch (GitAPIException | IOException e) {
762 throw new ManifestErrorException(e);
763 }
764 } else {
765 try (Gitit.html#Git">Git git = new Git(repo)) {
766 for (RepoProject proj : filteredProjects) {
767 addSubmodule(proj.getName(), proj.getUrl(), proj.getPath(),
768 proj.getRevision(), proj.getCopyFiles(),
769 proj.getLinkFiles(), git);
770 }
771 return git.commit().setMessage(RepoText.get().repoCommitMessage)
772 .call();
773 } catch (GitAPIException | IOException e) {
774 throw new ManifestErrorException(e);
775 }
776 }
777 }
778
779 private void addSubmodule(String name, String url, String path,
780 String revision, List<CopyFile> copyfiles, List<LinkFile> linkfiles,
781 Git git) throws GitAPIException, IOException {
782 assert (!repo.isBare());
783 assert (git != null);
784 if (!linkfiles.isEmpty()) {
785 throw new UnsupportedOperationException(
786 JGitText.get().nonBareLinkFilesNotSupported);
787 }
788
789 SubmoduleAddCommand add = git.submoduleAdd().setName(name).setPath(path)
790 .setURI(url);
791 if (monitor != null)
792 add.setProgressMonitor(monitor);
793
794 Repository subRepo = add.call();
795 if (revision != null) {
796 try (Gitit.html#Git">Git sub = new Git(subRepo)) {
797 sub.checkout().setName(findRef(revision, subRepo)).call();
798 }
799 subRepo.close();
800 git.add().addFilepattern(path).call();
801 }
802 for (CopyFile copyfile : copyfiles) {
803 copyfile.copy();
804 git.add().addFilepattern(copyfile.dest).call();
805 }
806 }
807
808
809
810
811
812
813
814
815 private List<RepoProject> renameProjects(List<RepoProject> projects) {
816 Map<String, List<RepoProject>> m = new TreeMap<>();
817 for (RepoProject proj : projects) {
818 List<RepoProject> l = m.get(proj.getName());
819 if (l == null) {
820 l = new ArrayList<>();
821 m.put(proj.getName(), l);
822 }
823 l.add(proj);
824 }
825
826 List<RepoProject> ret = new ArrayList<>();
827 for (List<RepoProject> ps : m.values()) {
828 boolean nameConflict = ps.size() != 1;
829 for (RepoProject proj : ps) {
830 String name = proj.getName();
831 if (nameConflict) {
832 name += SLASH + proj.getPath();
833 }
834 RepoProject p = new RepoProject(name,
835 proj.getPath(), proj.getRevision(), null,
836 proj.getGroups(), proj.getRecommendShallow());
837 p.setUrl(proj.getUrl());
838 p.addCopyFiles(proj.getCopyFiles());
839 p.addLinkFiles(proj.getLinkFiles());
840 ret.add(p);
841 }
842 }
843 return ret;
844 }
845
846
847
848
849
850
851 private static final String SLASH = "/";
852 static URI relativize(URI current, URI target) {
853 if (!Objects.equals(current.getHost(), target.getHost())) {
854 return target;
855 }
856
857 String cur = current.normalize().getPath();
858 String dest = target.normalize().getPath();
859
860
861 if (cur.startsWith(SLASH) != dest.startsWith(SLASH)) {
862 return target;
863 }
864
865 while (cur.startsWith(SLASH)) {
866 cur = cur.substring(1);
867 }
868 while (dest.startsWith(SLASH)) {
869 dest = dest.substring(1);
870 }
871
872 if (cur.indexOf('/') == -1 || dest.indexOf('/') == -1) {
873
874 String prefix = "prefix/";
875 cur = prefix + cur;
876 dest = prefix + dest;
877 }
878
879 if (!cur.endsWith(SLASH)) {
880
881 int lastSlash = cur.lastIndexOf('/');
882 cur = cur.substring(0, lastSlash);
883 }
884 String destFile = "";
885 if (!dest.endsWith(SLASH)) {
886
887 int lastSlash = dest.lastIndexOf('/');
888 destFile = dest.substring(lastSlash + 1, dest.length());
889 dest = dest.substring(0, dest.lastIndexOf('/'));
890 }
891
892 String[] cs = cur.split(SLASH);
893 String[] ds = dest.split(SLASH);
894
895 int common = 0;
896 while (common < cs.length && common < ds.length && cs[common].equals(ds[common])) {
897 common++;
898 }
899
900 StringJoiner j = new StringJoiner(SLASH);
901 for (int i = common; i < cs.length; i++) {
902 j.add("..");
903 }
904 for (int i = common; i < ds.length; i++) {
905 j.add(ds[i]);
906 }
907
908 j.add(destFile);
909 return URI.create(j.toString());
910 }
911
912 private static String findRef(String ref, Repository repo)
913 throws IOException {
914 if (!ObjectId.isId(ref)) {
915 Ref r = repo.exactRef(R_REMOTES + DEFAULT_REMOTE_NAME + "/" + ref);
916 if (r != null)
917 return r.getName();
918 }
919 return ref;
920 }
921 }