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 org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME;
46 import static org.eclipse.jgit.lib.Constants.R_REMOTES;
47
48 import java.io.File;
49 import java.io.FileInputStream;
50 import java.io.IOException;
51 import java.io.InputStream;
52 import java.net.URI;
53 import java.text.MessageFormat;
54 import java.util.ArrayList;
55 import java.util.List;
56 import java.util.Map;
57 import java.util.Objects;
58 import java.util.Set;
59 import java.util.StringJoiner;
60
61 import org.eclipse.jgit.annotations.Nullable;
62 import org.eclipse.jgit.api.Git;
63 import org.eclipse.jgit.api.GitCommand;
64 import org.eclipse.jgit.api.SubmoduleAddCommand;
65 import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
66 import org.eclipse.jgit.api.errors.GitAPIException;
67 import org.eclipse.jgit.api.errors.JGitInternalException;
68 import org.eclipse.jgit.dircache.DirCache;
69 import org.eclipse.jgit.dircache.DirCacheBuilder;
70 import org.eclipse.jgit.dircache.DirCacheEntry;
71 import org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader;
72 import org.eclipse.jgit.gitrepo.RepoProject.CopyFile;
73 import org.eclipse.jgit.gitrepo.RepoProject.LinkFile;
74 import org.eclipse.jgit.gitrepo.internal.RepoText;
75 import org.eclipse.jgit.internal.JGitText;
76 import org.eclipse.jgit.lib.CommitBuilder;
77 import org.eclipse.jgit.lib.Config;
78 import org.eclipse.jgit.lib.Constants;
79 import org.eclipse.jgit.lib.FileMode;
80 import org.eclipse.jgit.lib.ObjectId;
81 import org.eclipse.jgit.lib.ObjectInserter;
82 import org.eclipse.jgit.lib.ObjectReader;
83 import org.eclipse.jgit.lib.PersonIdent;
84 import org.eclipse.jgit.lib.ProgressMonitor;
85 import org.eclipse.jgit.lib.Ref;
86 import org.eclipse.jgit.lib.RefDatabase;
87 import org.eclipse.jgit.lib.RefUpdate;
88 import org.eclipse.jgit.lib.RefUpdate.Result;
89 import org.eclipse.jgit.lib.Repository;
90 import org.eclipse.jgit.revwalk.RevCommit;
91 import org.eclipse.jgit.revwalk.RevWalk;
92 import org.eclipse.jgit.util.FileUtils;
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111 public class RepoCommand extends GitCommand<RevCommit> {
112 private String manifestPath;
113 private String baseUri;
114 private URI targetUri;
115 private String groupsParam;
116 private String branch;
117 private String targetBranch = Constants.HEAD;
118 private boolean recordRemoteBranch = false;
119 private boolean recordSubmoduleLabels = false;
120 private boolean recordShallowSubmodules = false;
121 private PersonIdent author;
122 private RemoteReader callback;
123 private InputStream inputStream;
124 private IncludedFileReader includedReader;
125 private boolean ignoreRemoteFailures = false;
126
127 private List<RepoProject> bareProjects;
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 bareProjects = new ArrayList<>();
523 if (author == null)
524 author = new PersonIdent(repo);
525 if (callback == null)
526 callback = new DefaultRemoteReader();
527 for (RepoProject proj : filteredProjects) {
528 addSubmoduleBare(proj.getUrl(), proj.getPath(),
529 proj.getRevision(), proj.getCopyFiles(),
530 proj.getLinkFiles(), proj.getGroups(),
531 proj.getRecommendShallow());
532 }
533 DirCache index = DirCache.newInCore();
534 DirCacheBuilder builder = index.builder();
535 ObjectInserter inserter = repo.newObjectInserter();
536 try (RevWalk rw = new RevWalk(repo)) {
537 Config cfg = new Config();
538 StringBuilder attributes = new StringBuilder();
539 for (RepoProject proj : bareProjects) {
540 String path = proj.getPath();
541 String nameUri = proj.getName();
542 ObjectId objectId;
543 if (ObjectId.isId(proj.getRevision())) {
544 objectId = ObjectId.fromString(proj.getRevision());
545 } else {
546 objectId = callback.sha1(nameUri, proj.getRevision());
547 if (objectId == null && !ignoreRemoteFailures) {
548 throw new RemoteUnavailableException(nameUri);
549 }
550 if (recordRemoteBranch) {
551
552 cfg.setString("submodule", path, "branch",
553 proj.getRevision());
554 }
555
556 if (recordShallowSubmodules && proj.getRecommendShallow() != null) {
557
558
559
560
561
562 cfg.setBoolean("submodule", path, "shallow",
563 true);
564 }
565 }
566 if (recordSubmoduleLabels) {
567 StringBuilder rec = new StringBuilder();
568 rec.append("/");
569 rec.append(path);
570 for (String group : proj.getGroups()) {
571 rec.append(" ");
572 rec.append(group);
573 }
574 rec.append("\n");
575 attributes.append(rec.toString());
576 }
577
578 URI submodUrl = URI.create(nameUri);
579 if (targetUri != null) {
580 submodUrl = relativize(targetUri, submodUrl);
581 }
582 cfg.setString("submodule", path, "path", path);
583 cfg.setString("submodule", path, "url", submodUrl.toString());
584
585
586 if (objectId != null) {
587 DirCacheEntry dcEntry = new DirCacheEntry(path);
588 dcEntry.setObjectId(objectId);
589 dcEntry.setFileMode(FileMode.GITLINK);
590 builder.add(dcEntry);
591
592 for (CopyFile copyfile : proj.getCopyFiles()) {
593 byte[] src = callback.readFile(
594 nameUri, proj.getRevision(), copyfile.src);
595 objectId = inserter.insert(Constants.OBJ_BLOB, src);
596 dcEntry = new DirCacheEntry(copyfile.dest);
597 dcEntry.setObjectId(objectId);
598 dcEntry.setFileMode(FileMode.REGULAR_FILE);
599 builder.add(dcEntry);
600 }
601 for (LinkFile linkfile : proj.getLinkFiles()) {
602 String link;
603 if (linkfile.dest.contains("/")) {
604 link = FileUtils.relativizeGitPath(
605 linkfile.dest.substring(0,
606 linkfile.dest.lastIndexOf('/')),
607 proj.getPath() + "/" + linkfile.src);
608 } else {
609 link = proj.getPath() + "/" + linkfile.src;
610 }
611
612 objectId = inserter.insert(Constants.OBJ_BLOB,
613 link.getBytes(
614 Constants.CHARACTER_ENCODING));
615 dcEntry = new DirCacheEntry(linkfile.dest);
616 dcEntry.setObjectId(objectId);
617 dcEntry.setFileMode(FileMode.SYMLINK);
618 builder.add(dcEntry);
619 }
620 }
621 }
622 String content = cfg.toText();
623
624
625 final DirCacheEntry dcEntry = new DirCacheEntry(Constants.DOT_GIT_MODULES);
626 ObjectId objectId = inserter.insert(Constants.OBJ_BLOB,
627 content.getBytes(Constants.CHARACTER_ENCODING));
628 dcEntry.setObjectId(objectId);
629 dcEntry.setFileMode(FileMode.REGULAR_FILE);
630 builder.add(dcEntry);
631
632 if (recordSubmoduleLabels) {
633
634 final DirCacheEntry dcEntryAttr = new DirCacheEntry(Constants.DOT_GIT_ATTRIBUTES);
635 ObjectId attrId = inserter.insert(Constants.OBJ_BLOB,
636 attributes.toString().getBytes(Constants.CHARACTER_ENCODING));
637 dcEntryAttr.setObjectId(attrId);
638 dcEntryAttr.setFileMode(FileMode.REGULAR_FILE);
639 builder.add(dcEntryAttr);
640 }
641
642 builder.finish();
643 ObjectId treeId = index.writeTree(inserter);
644
645
646 ObjectId headId = repo.resolve(targetBranch + "^{commit}");
647 if (headId != null && rw.parseCommit(headId).getTree().getId().equals(treeId)) {
648
649 return rw.parseCommit(headId);
650 }
651
652 CommitBuilder commit = new CommitBuilder();
653 commit.setTreeId(treeId);
654 if (headId != null)
655 commit.setParentIds(headId);
656 commit.setAuthor(author);
657 commit.setCommitter(author);
658 commit.setMessage(RepoText.get().repoCommitMessage);
659
660 ObjectId commitId = inserter.insert(commit);
661 inserter.flush();
662
663 RefUpdate ru = repo.updateRef(targetBranch);
664 ru.setNewObjectId(commitId);
665 ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId());
666 Result rc = ru.update(rw);
667
668 switch (rc) {
669 case NEW:
670 case FORCED:
671 case FAST_FORWARD:
672
673 break;
674 case REJECTED:
675 case LOCK_FAILURE:
676 throw new ConcurrentRefUpdateException(
677 MessageFormat.format(
678 JGitText.get().cannotLock, targetBranch),
679 ru.getRef(),
680 rc);
681 default:
682 throw new JGitInternalException(MessageFormat.format(
683 JGitText.get().updatingRefFailed,
684 targetBranch, commitId.name(), rc));
685 }
686
687 return rw.parseCommit(commitId);
688 } catch (GitAPIException | IOException e) {
689 throw new ManifestErrorException(e);
690 }
691 } else {
692 try (Git git = new Git(repo)) {
693 for (RepoProject proj : filteredProjects) {
694 addSubmodule(proj.getUrl(), proj.getPath(),
695 proj.getRevision(), proj.getCopyFiles(),
696 proj.getLinkFiles(), git);
697 }
698 return git.commit().setMessage(RepoText.get().repoCommitMessage)
699 .call();
700 } catch (GitAPIException | IOException e) {
701 throw new ManifestErrorException(e);
702 }
703 }
704 }
705
706 private void addSubmodule(String url, String path, String revision,
707 List<CopyFile> copyfiles, List<LinkFile> linkfiles, Git git)
708 throws GitAPIException, IOException {
709 assert (!repo.isBare());
710 assert (git != null);
711 if (!linkfiles.isEmpty()) {
712 throw new UnsupportedOperationException(
713 JGitText.get().nonBareLinkFilesNotSupported);
714 }
715
716 SubmoduleAddCommand add = git.submoduleAdd().setPath(path).setURI(url);
717 if (monitor != null)
718 add.setProgressMonitor(monitor);
719
720 Repository subRepo = add.call();
721 if (revision != null) {
722 try (Git sub = new Git(subRepo)) {
723 sub.checkout().setName(findRef(revision, subRepo)).call();
724 }
725 subRepo.close();
726 git.add().addFilepattern(path).call();
727 }
728 for (CopyFile copyfile : copyfiles) {
729 copyfile.copy();
730 git.add().addFilepattern(copyfile.dest).call();
731 }
732 }
733
734 private void addSubmoduleBare(String url, String path, String revision,
735 List<CopyFile> copyfiles, List<LinkFile> linkfiles,
736 Set<String> groups, String recommendShallow) {
737 assert (repo.isBare());
738 assert (bareProjects != null);
739 RepoProject proj = new RepoProject(url, path, revision, null, groups,
740 recommendShallow);
741 proj.addCopyFiles(copyfiles);
742 proj.addLinkFiles(linkfiles);
743 bareProjects.add(proj);
744 }
745
746
747
748
749
750
751 private static final String SLASH = "/";
752 static URI relativize(URI current, URI target) {
753 if (!Objects.equals(current.getHost(), target.getHost())) {
754 return target;
755 }
756
757 String cur = current.normalize().getPath();
758 String dest = target.normalize().getPath();
759
760
761 if (cur.startsWith(SLASH) != dest.startsWith(SLASH)) {
762 return target;
763 }
764
765 while (cur.startsWith(SLASH)) {
766 cur = cur.substring(1);
767 }
768 while (dest.startsWith(SLASH)) {
769 dest = dest.substring(1);
770 }
771
772 if (cur.indexOf('/') == -1 || dest.indexOf('/') == -1) {
773
774 String prefix = "prefix/";
775 cur = prefix + cur;
776 dest = prefix + dest;
777 }
778
779 if (!cur.endsWith(SLASH)) {
780
781 int lastSlash = cur.lastIndexOf('/');
782 cur = cur.substring(0, lastSlash);
783 }
784 String destFile = "";
785 if (!dest.endsWith(SLASH)) {
786
787 int lastSlash = dest.lastIndexOf('/');
788 destFile = dest.substring(lastSlash + 1, dest.length());
789 dest = dest.substring(0, dest.lastIndexOf('/'));
790 }
791
792 String[] cs = cur.split(SLASH);
793 String[] ds = dest.split(SLASH);
794
795 int common = 0;
796 while (common < cs.length && common < ds.length && cs[common].equals(ds[common])) {
797 common++;
798 }
799
800 StringJoiner j = new StringJoiner(SLASH);
801 for (int i = common; i < cs.length; i++) {
802 j.add("..");
803 }
804 for (int i = common; i < ds.length; i++) {
805 j.add(ds[i]);
806 }
807
808 j.add(destFile);
809 return URI.create(j.toString());
810 }
811
812 private static String findRef(String ref, Repository repo)
813 throws IOException {
814 if (!ObjectId.isId(ref)) {
815 Ref r = repo.exactRef(R_REMOTES + DEFAULT_REMOTE_NAME + "/" + ref);
816 if (r != null)
817 return r.getName();
818 }
819 return ref;
820 }
821 }