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