1
2
3
4
5
6
7
8
9
10 package org.eclipse.jgit.merge;
11
12 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
13
14 import java.io.Closeable;
15 import java.io.File;
16 import java.io.FileOutputStream;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.OutputStream;
20 import java.time.Instant;
21 import java.util.HashMap;
22 import java.util.LinkedList;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Objects;
26 import java.util.TreeMap;
27
28 import org.eclipse.jgit.annotations.NonNull;
29 import org.eclipse.jgit.annotations.Nullable;
30 import org.eclipse.jgit.attributes.Attribute;
31 import org.eclipse.jgit.attributes.Attributes;
32 import org.eclipse.jgit.dircache.DirCache;
33 import org.eclipse.jgit.dircache.DirCacheBuildIterator;
34 import org.eclipse.jgit.dircache.DirCacheBuilder;
35 import org.eclipse.jgit.dircache.DirCacheCheckout;
36 import org.eclipse.jgit.dircache.DirCacheCheckout.StreamSupplier;
37 import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata;
38 import org.eclipse.jgit.dircache.DirCacheEntry;
39 import org.eclipse.jgit.errors.IndexWriteException;
40 import org.eclipse.jgit.errors.NoWorkTreeException;
41 import org.eclipse.jgit.internal.JGitText;
42 import org.eclipse.jgit.lib.Config;
43 import org.eclipse.jgit.lib.ConfigConstants;
44 import org.eclipse.jgit.lib.CoreConfig.EolStreamType;
45 import org.eclipse.jgit.lib.FileMode;
46 import org.eclipse.jgit.lib.ObjectId;
47 import org.eclipse.jgit.lib.ObjectInserter;
48 import org.eclipse.jgit.lib.ObjectReader;
49 import org.eclipse.jgit.lib.Repository;
50 import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
51 import org.eclipse.jgit.treewalk.WorkingTreeOptions;
52 import org.eclipse.jgit.util.LfsFactory;
53 import org.eclipse.jgit.util.LfsFactory.LfsInputStream;
54 import org.eclipse.jgit.util.io.EolStreamTypeUtil;
55
56
57
58
59
60
61
62
63 class WorkTreeUpdater implements Closeable {
64
65
66
67
68 public static class Result {
69
70 private final List<String> modifiedFiles = new LinkedList<>();
71
72
73 private final List<String> failedToDelete = new LinkedList<>();
74
75 private ObjectId treeId = null;
76
77
78
79
80 public ObjectId getTreeId() {
81 return treeId;
82 }
83
84
85
86
87 public List<String> getFailedToDelete() {
88 return failedToDelete;
89 }
90
91
92
93
94 public List<String> getModifiedFiles() {
95 return modifiedFiles;
96 }
97 }
98
99 Result result = new Result();
100
101
102
103
104 @Nullable
105 private final Repository repo;
106
107
108
109
110
111
112
113
114 private final boolean inCore;
115
116 private final ObjectInserter inserter;
117
118 private final ObjectReader reader;
119
120 private DirCache dirCache;
121
122 private boolean implicitDirCache = false;
123
124
125
126
127 private DirCacheBuilder builder;
128
129
130
131
132
133 private WorkingTreeOptions workingTreeOptions;
134
135
136
137
138
139 private int inCoreFileSizeLimit;
140
141
142
143
144
145 private final Map<String, DirCacheEntry> toBeCheckedOut = new HashMap<>();
146
147
148
149
150
151 private final TreeMap<String, File> toBeDeleted = new TreeMap<>();
152
153
154
155
156 private Map<String, CheckoutMetadata> checkoutMetadataByPath;
157
158
159
160
161 private Map<String, CheckoutMetadata> cleanupMetadataByPath;
162
163
164
165
166 private boolean indexChangesWritten;
167
168
169
170
171
172
173
174
175 private WorkTreeUpdater(Repository repo, DirCache dirCache) {
176 this.repo = repo;
177 this.dirCache = dirCache;
178
179 this.inCore = false;
180 this.inserter = repo.newObjectInserter();
181 this.reader = inserter.newReader();
182 Config config = repo.getConfig();
183 this.workingTreeOptions = config.get(WorkingTreeOptions.KEY);
184 this.inCoreFileSizeLimit = getInCoreFileSizeLimit(config);
185 this.checkoutMetadataByPath = new HashMap<>();
186 this.cleanupMetadataByPath = new HashMap<>();
187 }
188
189
190
191
192
193
194
195
196
197
198
199 public static WorkTreeUpdater createWorkTreeUpdater(Repository repo,
200 DirCache dirCache) {
201 return new WorkTreeUpdater(repo, dirCache);
202 }
203
204
205
206
207
208
209
210
211
212
213 private WorkTreeUpdater(Repository repo, DirCache dirCache,
214 ObjectInserter oi) {
215 this.repo = repo;
216 this.dirCache = dirCache;
217 this.inserter = oi;
218
219 this.inCore = true;
220 this.reader = oi.newReader();
221 if (repo != null) {
222 this.inCoreFileSizeLimit = getInCoreFileSizeLimit(repo.getConfig());
223 }
224 }
225
226
227
228
229
230
231
232
233
234
235
236
237
238 public static WorkTreeUpdater createInCoreWorkTreeUpdater(Repository repo,
239 DirCache dirCache, ObjectInserter oi) {
240 return new WorkTreeUpdater(repo, dirCache, oi);
241 }
242
243 private static int getInCoreFileSizeLimit(Config config) {
244 return config.getInt(ConfigConstants.CONFIG_MERGE_SECTION,
245 ConfigConstants.CONFIG_KEY_IN_CORE_LIMIT, 10 << 20);
246 }
247
248
249
250
251
252
253 public int getInCoreFileSizeLimit() {
254 return inCoreFileSizeLimit;
255 }
256
257
258
259
260
261
262
263
264 public DirCache getLockedDirCache() throws IOException {
265 if (dirCache == null) {
266 implicitDirCache = true;
267 if (inCore) {
268 dirCache = DirCache.newInCore();
269 } else {
270 dirCache = nonNullRepo().lockDirCache();
271 }
272 }
273 if (builder == null) {
274 builder = dirCache.builder();
275 }
276 return dirCache;
277 }
278
279
280
281
282
283
284
285 public DirCacheBuildIterator createDirCacheBuildIterator() {
286 return new DirCacheBuildIterator(builder);
287 }
288
289
290
291
292
293
294
295
296
297 public void writeWorkTreeChanges(boolean shouldCheckoutTheirs)
298 throws IOException {
299 handleDeletedFiles();
300
301 if (inCore) {
302 builder.finish();
303 return;
304 }
305 if (shouldCheckoutTheirs) {
306
307
308
309 checkout();
310 }
311
312
313
314
315
316 if (!builder.commit()) {
317 revertModifiedFiles();
318 throw new IndexWriteException();
319 }
320 }
321
322
323
324
325
326
327
328
329 public Result writeIndexChanges() throws IOException {
330 result.treeId = getLockedDirCache().writeTree(inserter);
331 indexChangesWritten = true;
332 return result;
333 }
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352 public void addToCheckout(String path, DirCacheEntry entry,
353 EolStreamType cleanupStreamType, String cleanupSmudgeCommand,
354 EolStreamType checkoutStreamType, String checkoutSmudgeCommand) {
355 if (entry != null) {
356
357 toBeCheckedOut.put(path, entry);
358 }
359 addCheckoutMetadata(cleanupMetadataByPath, path, cleanupStreamType,
360 cleanupSmudgeCommand);
361 addCheckoutMetadata(checkoutMetadataByPath, path, checkoutStreamType,
362 checkoutSmudgeCommand);
363 }
364
365
366
367
368
369
370
371
372
373
374
375 public Map<String, DirCacheEntry> getToBeCheckedOut() {
376 return toBeCheckedOut;
377 }
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393 public void deleteFile(String path, File file, EolStreamType streamType,
394 String smudgeCommand) {
395 toBeDeleted.put(path, file);
396 if (file != null && file.isFile()) {
397 addCheckoutMetadata(cleanupMetadataByPath, path, streamType,
398 smudgeCommand);
399 }
400 }
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415 private void addCheckoutMetadata(Map<String, CheckoutMetadata> map,
416 String path, EolStreamType streamType, String smudgeCommand) {
417 if (inCore || map == null) {
418 return;
419 }
420 map.put(path, new CheckoutMetadata(streamType, smudgeCommand));
421 }
422
423
424
425
426
427
428
429
430
431
432
433 public EolStreamType detectCheckoutStreamType(Attributes attributes) {
434 if (inCore) {
435 return null;
436 }
437 return EolStreamTypeUtil.detectStreamType(OperationType.CHECKOUT_OP,
438 workingTreeOptions, attributes);
439 }
440
441 private void handleDeletedFiles() {
442
443
444
445 for (String path : toBeDeleted.descendingKeySet()) {
446 File file = inCore ? null : toBeDeleted.get(path);
447 if (file != null && !file.delete()) {
448 if (!file.isDirectory()) {
449 result.failedToDelete.add(path);
450 }
451 }
452 }
453 }
454
455
456
457
458
459
460
461 public void markAsModified(String path) {
462 result.modifiedFiles.add(path);
463 }
464
465
466
467
468
469
470 public List<String> getModifiedFiles() {
471 return result.modifiedFiles;
472 }
473
474 private void checkout() throws NoWorkTreeException, IOException {
475 for (Map.Entry<String, DirCacheEntry> entry : toBeCheckedOut
476 .entrySet()) {
477 DirCacheEntry dirCacheEntry = entry.getValue();
478 if (dirCacheEntry.getFileMode() == FileMode.GITLINK) {
479 new File(nonNullRepo().getWorkTree(), entry.getKey())
480 .mkdirs();
481 } else {
482 DirCacheCheckout.checkoutEntry(repo, dirCacheEntry, reader,
483 false, checkoutMetadataByPath.get(entry.getKey()),
484 workingTreeOptions);
485 result.modifiedFiles.add(entry.getKey());
486 }
487 }
488 }
489
490
491
492
493
494
495
496
497
498
499 public void revertModifiedFiles() throws IOException {
500 if (inCore) {
501 result.modifiedFiles.clear();
502 return;
503 }
504 if (indexChangesWritten) {
505 return;
506 }
507 for (String path : result.modifiedFiles) {
508 DirCacheEntry entry = dirCache.getEntry(path);
509 if (entry != null) {
510 DirCacheCheckout.checkoutEntry(repo, entry, reader, false,
511 cleanupMetadataByPath.get(path), workingTreeOptions);
512 }
513 }
514 }
515
516 @Override
517 public void close() throws IOException {
518 if (implicitDirCache) {
519 dirCache.unlock();
520 }
521 }
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539 public void updateFileWithContent(StreamSupplier inputStream,
540 EolStreamType streamType, String smudgeCommand, String path,
541 File file) throws IOException {
542 if (inCore) {
543 return;
544 }
545 CheckoutMetadata metadata = new CheckoutMetadata(streamType,
546 smudgeCommand);
547
548 try (OutputStream outputStream = new FileOutputStream(file)) {
549 DirCacheCheckout.getContent(repo, path, metadata,
550 inputStream, workingTreeOptions, outputStream);
551 }
552 }
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576 public DirCacheEntry insertToIndex(InputStream input,
577 byte[] path, FileMode fileMode, int entryStage,
578 Instant lastModified, int len, Attribute lfsAttribute)
579 throws IOException {
580 return addExistingToIndex(insertResult(input, lfsAttribute, len), path,
581 fileMode, entryStage, lastModified, len);
582 }
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601 public DirCacheEntry addExistingToIndex(ObjectId objectId, byte[] path,
602 FileMode fileMode, int entryStage, Instant lastModified, int len) {
603 DirCacheEntry dce = new DirCacheEntry(path, entryStage);
604 dce.setFileMode(fileMode);
605 if (lastModified != null) {
606 dce.setLastModified(lastModified);
607 }
608 dce.setLength(inCore ? 0 : len);
609 dce.setObjectId(objectId);
610 builder.add(dce);
611 return dce;
612 }
613
614 private ObjectId insertResult(InputStream input,
615 Attribute lfsAttribute, long length) throws IOException {
616 try (LfsInputStream is = LfsFactory.getInstance().applyCleanFilter(repo,
617 input, length,
618 lfsAttribute)) {
619 return inserter.insert(OBJ_BLOB, is.getLength(), is);
620 }
621 }
622
623
624
625
626
627
628
629
630 @NonNull
631 private Repository nonNullRepo() throws NullPointerException {
632 return Objects.requireNonNull(repo,
633 () -> JGitText.get().repositoryIsRequired);
634 }
635 }