Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: 6ff79882a764736a9bbd422594266697d7637412 (plain) (blame)
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
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
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
/*******************************************************************************
 * Copyright (c) 2000, 2008 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.team.internal.ccvs.core.resources;

import java.util.*;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.*;
import org.eclipse.osgi.util.NLS;
import org.eclipse.team.internal.ccvs.core.*;
import org.eclipse.team.internal.ccvs.core.client.*;
import org.eclipse.team.internal.ccvs.core.client.Command.*;
import org.eclipse.team.internal.ccvs.core.client.listeners.*;
import org.eclipse.team.internal.ccvs.core.connection.CVSRepositoryLocation;
import org.eclipse.team.internal.ccvs.core.connection.CVSServerException;
import org.eclipse.team.internal.ccvs.core.syncinfo.FolderSyncInfo;
import org.eclipse.team.internal.ccvs.core.syncinfo.ResourceSyncInfo;
import org.eclipse.team.internal.ccvs.core.util.Util;

/*
 * This class is responsible for building a remote tree that shows the repository
 * state of a locally loaded folder tree.
 * 
 * It is used as follows
 * 
 * 		RemoteFolderTreeBuilder.buildRemoteTree(CVSRepositoryLocation, IManagedFolder, String, IProgressMonitor);
 * 
 * The provider IManagedFolder can be a local resource or a RemoteFolderTree that
 * that was previously built.
 */
public class RemoteFolderTreeBuilder {

	private static final int MAX_REVISION_FETCHES_PER_CONNECTION = 1024;
	
	private Map<String, Map> fileDeltas;
	private List<String> changedFiles;
	private Map<String, RemoteFolderTree> remoteFolderTable;
	
	private ICVSFolder root;
	private RemoteFolderTree remoteRoot;
	private CVSRepositoryLocation repository;
	
	private CVSTag tag;
	
	private LocalOption[] updateLocalOptions;
	
	private boolean rootDoesNotExist = false;
	
	private static String UNKNOWN = ""; //$NON-NLS-1$
	private static String DELETED = "DELETED"; //$NON-NLS-1$
	private static String ADDED = "ADDED"; //$NON-NLS-1$
	private static String FOLDER = "FOLDER"; //$NON-NLS-1$
	
	private static Map EMPTY_MAP = new HashMap();
	
	private boolean newFolderExist = false;
	
	static class DeltaNode {
		int syncState = Update.STATE_NONE;
		String name;
		String revision;
		
		DeltaNode(String name, String revision, int syncState) {
			this.name = name;
			this.revision = revision;
			this.syncState = syncState;			
		}
		
		String getName() {
			return name;
		}
		
		String getRevision() {
			return revision;
		}
		
		int getSyncState() {
			return syncState;
		}
	}
		
	
	/* package */ RemoteFolderTreeBuilder(CVSRepositoryLocation repository, ICVSFolder root, CVSTag tag) {
		this.repository = repository;
		this.root = root;
		this.tag = tag;
		this.fileDeltas = new HashMap<String, Map>();
		this.changedFiles = new ArrayList<String>();
		this.remoteFolderTable = new HashMap<String, RemoteFolderTree>();
		
		// Build the local options
		List<LocalOption> localOptions = new ArrayList<>();
		if (tag != null) {
			if (tag.getType() == CVSTag.HEAD) {
				localOptions.add(Update.CLEAR_STICKY);
			} else {
				localOptions.add(Update.makeTagOption(tag));
			}
		}
		updateLocalOptions = localOptions.toArray(new LocalOption[localOptions.size()]);
	}
	
	private LocalOption[] getOptionsWithoutTag() {
		// Build the local options
		List<LocalOption> localOptions = new ArrayList<>();
		localOptions.add(Update.RETRIEVE_ABSENT_DIRECTORIES);
		return localOptions.toArray(new LocalOption[localOptions.size()]);
	}
	
	public static RemoteFolder buildBaseTree(CVSRepositoryLocation repository, ICVSFolder root, CVSTag tag, IProgressMonitor progress) throws CVSException {
		try {
			RemoteFolderTreeBuilder builder = new RemoteFolderTreeBuilder(repository, root, tag);
			progress.beginTask(null, 100);
			IProgressMonitor subProgress = Policy.infiniteSubMonitorFor(progress, 100);
			subProgress.beginTask(null, 512);  
			subProgress.subTask(NLS.bind(CVSMessages.RemoteFolderTreeBuilder_buildingBase, new String[] { root.getName() })); 
			return builder.buildBaseTree(null, root, subProgress);
		} finally {
			progress.done();
		}
	}
	
	public static RemoteFolderTree buildRemoteTree(CVSRepositoryLocation repository, IContainer root, CVSTag tag, IProgressMonitor monitor) throws CVSException {
		return buildRemoteTree(repository, CVSWorkspaceRoot.getCVSFolderFor(root), tag, monitor);
	}
	
	public static RemoteFolderTree buildRemoteTree(CVSRepositoryLocation repository, ICVSFolder root, CVSTag tag, IProgressMonitor monitor) throws CVSException {
		RemoteFolderTreeBuilder builder = new RemoteFolderTreeBuilder(repository, root, tag);
		return builder.buildTree(new ICVSResource[] { root }, monitor);
	}
	public static RemoteFile buildRemoteTree(CVSRepositoryLocation repository, ICVSFile file, CVSTag tag, IProgressMonitor monitor) throws CVSException {
		RemoteFolderTreeBuilder builder = new RemoteFolderTreeBuilder(repository, file.getParent(), tag);
		return builder.buildTree(file, monitor);
	}
	
	/* package */ RemoteFolderTree buildTree(ICVSResource[] resources, IProgressMonitor monitor) throws CVSException {
		
		// Make sure that the cvs commands are not quiet during this operations
		QuietOption quietness = CVSProviderPlugin.getPlugin().getQuietness();
		try {
			CVSProviderPlugin.getPlugin().setQuietness(Command.VERBOSE);
			
			monitor.beginTask(null, 100);

			// 1st Connection: Use local state to determine delta with server
			if (!fetchDelta(resources, Policy.subMonitorFor(monitor, 75))) {
				return null;
			}
			
			// 2nd Connection: Build remote tree from above delta using 2nd connection to fetch unknown directories
			// NOTE: Multiple commands may be issued over this connection.
			fetchNewDirectories(Policy.subMonitorFor(monitor, 10));

			//	3rd+ Connection: Used to fetch file status in groups of 1024
			fetchFileRevisions(Policy.subMonitorFor(monitor, 15));
			
			return remoteRoot;
			
		} finally {
			CVSProviderPlugin.getPlugin().setQuietness(quietness);
			monitor.done();
		}
	}

	private boolean fetchDelta(ICVSResource[] resources, IProgressMonitor monitor) throws CVSException {
		
		// Get the arguments from the files
		ArrayList<String> arguments = new ArrayList<>();
		for (int i = 0; i < resources.length; i++) {
			ICVSResource resource = resources[i];
			arguments.add(resource.getRelativePath(root));
		}
		
		// Use local state to determine delta with server
		monitor.beginTask(null, 100);
		Policy.checkCanceled(monitor);
		Session session = new Session(repository, root, false);
		session.open(Policy.subMonitorFor(monitor, 10), false /* read-only */);
		try {
			Policy.checkCanceled(monitor);
			fetchDelta(session, arguments.toArray(new String[arguments.size()]), Policy.subMonitorFor(monitor, 90));
			if (rootDoesNotExist) {
				// We cannot handle the case where a project (i.e. the top-most CVS folder)
				// has been deleted directly on the sever (i.e. deleted using rm -rf)
				if (root.isCVSFolder() && ! root.isManaged()) {
					IStatus status = new CVSStatus(IStatus.ERROR, CVSStatus.ERROR, NLS.bind(CVSMessages.RemoteFolderTreeBuild_folderDeletedFromServer, new String[] { root.getFolderSyncInfo().getRepository() }),root);
					throw new CVSException(status); 
				} else {
					return false;
				}
			}
		} finally {
			session.close();
			monitor.done();
		}
		return true;
	}

	private void fetchNewDirectories(IProgressMonitor monitor) throws CVSException {
		// Build remote tree from the fetched delta using a new connection to fetch unknown directories
		// NOTE: Multiple commands may be issued over this connection.
		monitor.beginTask(null, 100);
		Session session;
		FolderSyncInfo folderSyncInfo = root.getFolderSyncInfo();
		if (folderSyncInfo == null) {
			// We've lost the mapping in the local workspace.
			// This could be due to the project being deleted.
			if (root.exists()) {
				IResource resource = root.getIResource();
				String path;
				if (resource == null) {
					path = root.getName();
				} else {
					path = resource.getFullPath().toString();
				}
				IStatus status = new CVSStatus(IStatus.ERROR, CVSStatus.ERROR, NLS.bind(CVSMessages.RemoteFolderTreeBuilder_0, new String[] { path }), root);
				throw new CVSException(status); 
			} else {
				// Just return. The remote tree will be null
				return;
			}
		}
		remoteRoot =
			new RemoteFolderTree(null, root.getName(), repository,
				folderSyncInfo.getRepository(),
				tagForRemoteFolder(root, tag));
		if (newFolderExist) {
			// New folders will require a connection for fetching their members
			session = new Session(repository, remoteRoot, false);
			session.open(Policy.subMonitorFor(monitor, 10), false /* read-only */);
		} else {
			session = null;
		}
		try {
			// Set up an infinite progress monitor for the recursive build
			IProgressMonitor subProgress = Policy.infiniteSubMonitorFor(monitor, 90);
			subProgress.beginTask(null, 512);
			// Build the remote tree
			buildRemoteTree(session, root, remoteRoot, "", subProgress); //$NON-NLS-1$
		} finally {
			if (session != null) {
				session.close();
			}
			monitor.done();
		}
	}
	
	private void fetchFileRevisions(IProgressMonitor monitor) throws CVSException {
		// 3rd+ Connection: Used to fetch file status in groups of 1024
		if (remoteRoot != null && !changedFiles.isEmpty()) {
			String[] allChangedFiles = changedFiles.toArray(new String[changedFiles.size()]);
			int iterations = (allChangedFiles.length / MAX_REVISION_FETCHES_PER_CONNECTION) 
				+ (allChangedFiles.length % MAX_REVISION_FETCHES_PER_CONNECTION == 0 ? 0 : 1);
			for (int i = 0; i < iterations ; i++) {
				int length = Math.min(MAX_REVISION_FETCHES_PER_CONNECTION, 
					allChangedFiles.length - (MAX_REVISION_FETCHES_PER_CONNECTION * i));
				String buffer[] = new String[length];
				System.arraycopy(allChangedFiles, i * MAX_REVISION_FETCHES_PER_CONNECTION, buffer, 0, length);
				Session session = new Session(repository, remoteRoot, false);
				session.open(Policy.subMonitorFor(monitor, 1), false /* read-only */);
				try {
					fetchFileRevisions(session, buffer, Policy.subMonitorFor(monitor, 2));
				} finally {
					session.close();
				}
			}
		}
	}
	
	/* package */ RemoteFile buildTree(ICVSFile file, IProgressMonitor monitor) throws CVSException {
		QuietOption quietness = CVSProviderPlugin.getPlugin().getQuietness();
		try {
			CVSProviderPlugin.getPlugin().setQuietness(Command.VERBOSE);
			
			monitor.beginTask(null, 100);
	
			// Query the server to see if there is a delta available
			Policy.checkCanceled(monitor);
			Session session = new Session(repository, root, false);
			session.open(Policy.subMonitorFor(monitor, 10), false /* read-only */);
			try {
				Policy.checkCanceled(monitor);
				fetchDelta(session, new String[] { file.getName() }, Policy.subMonitorFor(monitor, 50));
				if (rootDoesNotExist) {
					return null;
				}
			} finally {
				session.close();
			}
			// Create a parent for the remote resource
			remoteRoot =
				new RemoteFolderTree(null, root.getName(), repository,
					root.getFolderSyncInfo().getRepository(),
					tagForRemoteFolder(root, tag));
			// Create the remote resource (using the delta if there is one)
			RemoteFile remoteFile;
			Map deltas = fileDeltas.get(""); //$NON-NLS-1$
			if (deltas == null || deltas.isEmpty()) {
				// If the file is an addition, return null as the remote
				// Note: If there was a conflicting addition, the delta would not be empty
				byte[] syncBytes = file.getSyncBytes();
				if ( syncBytes == null || ResourceSyncInfo.isAddition(syncBytes)) {
					return null;
				}
				remoteFile = new RemoteFile(remoteRoot, syncBytes);
			} else {
				DeltaNode d = (DeltaNode)deltas.get(file.getName());
				if (d.getRevision() == DELETED) {
					return null;
				}
				CVSTag newTag = tagForRemoteFolder(remoteRoot, tag);
				if (newTag == null && file.getSyncInfo() != null) {
					newTag = file.getSyncInfo().getTag();
				}
				remoteFile = new RemoteFile(remoteRoot, 
					d.getSyncState(), 
					file.getName(), 
					null, /* the revision will be retrieved from the server */
					getKeywordMode(file), /* use the same keyword mode as the local file */
					newTag);
			}
			// Add the resource to its parent
			remoteRoot.setChildren(new ICVSRemoteResource[] {remoteFile});
			// If there was a delta, fetch the new revision
			if (!changedFiles.isEmpty()) {
				// Add the remote folder to the remote folder lookup table (used to update file revisions)
				recordRemoteFolder(remoteRoot);
				session = new Session(repository, remoteRoot, false);
				session.open(Policy.subMonitorFor(monitor, 10), false /* read-only */);
				try {
					fetchFileRevisions(session, changedFiles.toArray(new String[changedFiles.size()]), Policy.subMonitorFor(monitor, 20));
				} finally {
					session.close();
				}
			}
			return remoteFile;
			
		} finally {
			CVSProviderPlugin.getPlugin().setQuietness(quietness);
			monitor.done();
		}
	}
	
	private Command.KSubstOption getKeywordMode(ICVSFile file) throws CVSException {
		if (file == null) return null;
		byte[] syncBytes = file.getSyncBytes();
		if (syncBytes == null) return null;
		return ResourceSyncInfo.getKeywordMode(syncBytes);
	}

	/*
	 * Build the base remote tree from the local tree.
	 * 
	 * The localPath is used to retrieve deltas from the recorded deltas
	 * 
	 * Does 1 work for each managed file and folder
	 */
	RemoteFolder buildBaseTree(RemoteFolder parent, ICVSFolder local, IProgressMonitor monitor) throws CVSException {
		
		Policy.checkCanceled(monitor);
					
		// Create a remote folder tree corresponding to the local resource
		FolderSyncInfo folderSyncInfo = local.getFolderSyncInfo();
		if (folderSyncInfo == null) return null;
		RemoteFolder remote = createRemoteFolder(local, parent, folderSyncInfo);

		// Create a List to contain the created children
		List<RemoteResource> children = new ArrayList<>();
		
		// Build the child folders corresponding to local folders base
		ICVSResource[] folders = local.members(ICVSFolder.FOLDER_MEMBERS);
		for (int i=0;i<folders.length;i++) {
			ICVSFolder folder = (ICVSFolder)folders[i];
			if (folder.isManaged() && folder.isCVSFolder()) {
				monitor.worked(1);
				RemoteFolder tree = buildBaseTree(remote, folder, monitor);
				if (tree != null)
					children.add(tree);
			}
		}
		
		// Build the child files corresponding to local files base
		ICVSResource[] files = local.members(ICVSFolder.FILE_MEMBERS);
		for (int i=0;i<files.length;i++) {
			ICVSFile file = (ICVSFile)files[i];
			byte[] syncBytes = file.getSyncBytes();
			// if there is no sync info then there is no base
			if (syncBytes==null)
				continue;
			// There is no remote if the file was added
			if (ResourceSyncInfo.isAddition(syncBytes))
				continue;
			// If the file was deleted locally, we need to generate a new sync info without the delete flag
			if (ResourceSyncInfo.isDeletion(syncBytes)) {
				syncBytes = ResourceSyncInfo.convertFromDeletion(syncBytes);
			}
			children.add(createRemoteFile(remote, syncBytes));
			monitor.worked(1);
		}
		
		// Remove any folders that are phantoms locally if they have no children
		if (children.isEmpty() && isPruneEmptyDirectories() && !local.exists())
			return null;

		// Add the children to the remote folder tree
		remote.setChildren(children.toArray(new ICVSRemoteResource[children.size()]));
		
		return remote;
	}

	protected RemoteFile createRemoteFile(RemoteFolder remote, byte[] syncBytes) throws CVSException {
		return new RemoteFile(remote, syncBytes);
	}

	protected RemoteFolder createRemoteFolder(ICVSFolder local, RemoteFolder parent, FolderSyncInfo folderSyncInfo) {
		return new RemoteFolderTree(parent, local.getName(), repository, folderSyncInfo.getRepository(), folderSyncInfo.getTag());
	}
	
	/*
	 * Build the remote tree from the local tree and the recorded deltas.
	 * 
	 * The localPath is used to retrieve deltas from the recorded deltas
	 * 
	 * Does 1 work for each file and folder delta processed
	 */
	private void buildRemoteTree(Session session, ICVSFolder local, RemoteFolderTree remote, String localPath, IProgressMonitor monitor) throws CVSException {
		
		Policy.checkCanceled(monitor);
		
		// Add the remote folder to the remote folder lookup table (used to update file revisions)
		recordRemoteFolder(remote);
		
		// Create a map to contain the created children
		Map<String, RemoteResource> children = new HashMap<>();
		
		// If there's no corresponding local resource then we need to fetch its contents in order to populate the deltas
		if (local == null) {
			fetchNewDirectory(session, remote, localPath, monitor);
		}
		
		// Fetch the delta's for the folder
		Map deltas = fileDeltas.get(localPath);
		if (deltas == null)
			deltas = EMPTY_MAP;
		
		// If there is a local, use the local children to start building the remote children
		if (local != null) {
			// Build the child folders corresponding to local folders
			ICVSResource[] folders = local.members(ICVSFolder.FOLDER_MEMBERS);
			for (int i=0;i<folders.length;i++) {
				ICVSFolder folder = (ICVSFolder)folders[i];
				DeltaNode d = (DeltaNode)deltas.get(folder.getName());
				if (folder.isCVSFolder() && ! isOrphanedSubtree(folder) && (d==null || d.getRevision() != DELETED)) {
					children.put(folders[i].getName(), 
						new RemoteFolderTree(remote, folders[i].getName(), repository, 
							folder.getFolderSyncInfo().getRepository(), 
							tagForRemoteFolder(folder,tag)));
				}
			}
			// Build the child files corresponding to local files
			ICVSResource[] files = local.members(ICVSFolder.FILE_MEMBERS);
			for (int i=0;i<files.length;i++) {
				ICVSFile file = (ICVSFile)files[i];

				DeltaNode d = (DeltaNode)deltas.get(file.getName());
				byte[] syncBytes = file.getSyncBytes();
				// if there is no sync info then there isn't a remote file for this local file on the
				// server.
				if (syncBytes==null)
					continue;
				// There is no remote if the file was added and we didn't get a conflict (C) indicator from the server
				if (ResourceSyncInfo.isAddition(syncBytes) && d==null)
					continue;
				// There is no remote if the file was deleted and we didn't get a remove (R) indicator from the server
				if (ResourceSyncInfo.isDeletion(syncBytes) && d==null)
					continue;
					
				int type = d==null ? Update.STATE_NONE : d.getSyncState();
				children.put(file.getName(), new RemoteFile(remote, type, syncBytes));
			}
		}
		
		// Build the children for new or out-of-date resources from the deltas
		Iterator i = deltas.keySet().iterator();
		while (i.hasNext()) {
			String name = (String)i.next();
			DeltaNode d = (DeltaNode)deltas.get(name);
			String revision = d.getRevision();
			if (revision == FOLDER) {
				children.put(name, new RemoteFolderTree(remote, repository, 
					Util.appendPath(remote.getRepositoryRelativePath(), name), 
					tagForRemoteFolder(remote, tag)));
			} else if (revision == ADDED) {
				children.put(name, new RemoteFile(remote, 
					d.getSyncState(), 
					name, 
					null, /* the revision will be fetched later */
					null, /* there's no way to know the remote keyword mode */
					tagForRemoteFolder(remote, tag)));
			} else if (revision == UNKNOWN) {
				// The local resource is out of sync with the remote.
				// Create a RemoteFile associated with the tag so we are assured of getting the proper revision
				// (Note: this will replace the RemoteFile added from the local base)
				children.put(name, new RemoteFile(remote, 
					d.getSyncState(), 
					name, 
					null, /* the revision will be fetched later */
					getKeywordMode((ICVSFile)children.get(name)), /* get the keyword mode from the local file*/
					tagForRemoteFolder(remote, tag)));
			} else if (revision == DELETED) {
				// This should have been deleted while creating from the local resources.
				// If it wasn't, delete it now.
				if (children.containsKey(name))
					children.remove(name);
			} else {
				// We should never get here
			}
			monitor.worked(1);
		}

		// Add the children to the remote folder tree
		remote.setChildren(children.values().toArray(new ICVSRemoteResource[children.size()]));
		
		// We have to delay building the child folders to support the proper fetching of new directories
		// due to the fact that the same CVS home directory (i.e. the same root directory) must
		// be used for all requests sent over the same connection
		Iterator childIterator = children.entrySet().iterator();
		List<RemoteFolderTree> emptyChildren = new ArrayList<>();
		while (childIterator.hasNext()) {
			Map.Entry entry = (Map.Entry)childIterator.next();
			if (((RemoteResource)entry.getValue()).isFolder()) {
				RemoteFolderTree remoteFolder = (RemoteFolderTree)entry.getValue();
				String name = (String)entry.getKey();
				ICVSFolder localFolder;
				DeltaNode d = (DeltaNode)deltas.get(name);
				// for directories that are new on the server 
				if (d!=null && d.getRevision() == FOLDER)
					localFolder = null;
				else
					localFolder = local.getFolder(name);
				buildRemoteTree(session, localFolder, remoteFolder, Util.appendPath(localPath, name), monitor);
				// Record any children that are empty
				if (isPruneEmptyDirectories() && remoteFolder.getChildren().length == 0) {
					// Prune if the local folder is also empty.
					if (localFolder == null || (localFolder.members(ICVSFolder.ALL_EXISTING_MEMBERS).length == 0))
						emptyChildren.add(remoteFolder);
					else {
						// Also prune if the tag we are fetching is not HEAD and differs from the tag of the local folder
						FolderSyncInfo info = localFolder.getFolderSyncInfo();
						if (tag != null && info != null && ! tag.equals(CVSTag.DEFAULT) && ! tag.equals(info.getTag()))
							emptyChildren.add(remoteFolder);
					}
				}
			}
		}
		
		// Prune any empty child folders
		if (isPruneEmptyDirectories() && !emptyChildren.isEmpty()) {
			List<ICVSRemoteResource> newChildren = new ArrayList<ICVSRemoteResource>();
			newChildren.addAll(Arrays.asList(remote.getChildren()));
			newChildren.removeAll(emptyChildren);
			remote.setChildren(newChildren.toArray(new ICVSRemoteResource[newChildren.size()]));

		}
	}
	
	/*
	 * This method fetches the delta between the local state and the remote state of the resource tree
	 * and records the deltas in the fileDeltas instance variable
	 * 
	 * Returns the list of changed files
	 */
	private List<String> fetchDelta(Session session, String[] arguments, final IProgressMonitor monitor) throws CVSException {
		
		// Create an listener that will accumulate new and removed files and folders
		IUpdateMessageListener listener = new IUpdateMessageListener() {
			public void directoryInformation(ICVSFolder root, String path, boolean newDirectory) {
				if (newDirectory) {
					// Record new directory with parent so it can be retrieved when building the parent
					recordDelta(path, FOLDER, Update.STATE_NONE);
					monitor.subTask(NLS.bind(CVSMessages.RemoteFolderTreeBuilder_receivingDelta, new String[] { Util.toTruncatedPath(path, 3) })); 
				}
			}
			public void directoryDoesNotExist(ICVSFolder root, String path) {
				// Record removed directory with parent so it can be removed when building the parent
				if (path.length() == 0) {
					rootDoesNotExist = true;
				} else {
					recordDelta(path, DELETED, Update.STATE_NONE);
					monitor.subTask(NLS.bind(CVSMessages.RemoteFolderTreeBuilder_receivingDelta, new String[] { Util.toTruncatedPath(path, 3) })); 
				}
			}
			public void fileInformation(int type, ICVSFolder root, String filename) {
				// Cases that do not require action are:
				//	case 'A' :  = A locally added file that does not exists remotely
				//	case '?' :  = A local file that has not been added and does not exists remotely
				//  case 'M' :  = A locally modified file that has not been modified remotely
				switch(type) {
					case Update.STATE_MERGEABLE_CONFLICT :
					case Update.STATE_CONFLICT : 
								// We have an remote change to a modified local file
								// The change could be a local change conflicting with a remote deletion.
								// If so, the deltas may already have a DELETED for the file.
								// We shouldn't override this DELETED
								Map deltas = fileDeltas.get(Util.removeLastSegment(filename));
								DeltaNode d = deltas != null ? (DeltaNode)deltas.get(Util.getLastSegment(filename)) : null;
								if ((d!=null) && (d.getRevision() == DELETED))
									break;
					case Update.STATE_DELETED : // We have a locally removed file that still exists remotely
					case Update.STATE_REMOTE_CHANGES : // We have an remote change to an unmodified local file
								changedFiles.add(filename);
								recordDelta(filename, UNKNOWN, type);
								monitor.subTask(NLS.bind(CVSMessages.RemoteFolderTreeBuilder_receivingDelta, new String[] { Util.toTruncatedPath(filename, 3) })); 
								break;
				}	
			}
			public void fileDoesNotExist(ICVSFolder root, String filename) {
				recordDelta(filename, DELETED, Update.STATE_NONE);
				monitor.subTask(NLS.bind(CVSMessages.RemoteFolderTreeBuilder_receivingDelta, new String[] { Util.toTruncatedPath(filename, 3) })); 
			}
		};
		
		// Perform a "cvs -n update -d [-r tag] ." in order to get the
		// messages from the server that will indicate what has changed on the 
		// server.
		IStatus status = Command.SYNCUPDATE.execute(session,
			new GlobalOption[] { Command.DO_NOT_CHANGE },
			updateLocalOptions,
			arguments,
			new UpdateListener(listener),
			monitor);
		if (status.getCode() == CVSStatus.SERVER_ERROR) {
			CVSServerException e = new CVSServerException(status);
			if (e.isNoTagException()) {
				// This error indicates that the complete subtree 
				// being fetched does not have any files for the tag being queried
				rootDoesNotExist = true;
			} else if (e.containsErrors()) {
				// Log the error
				CVSProviderPlugin.log(e);
			}
		}
		return changedFiles;
	}
	/*
	 * Fetch the children of a previously unknown directory.
	 * 
	 * The fetch may do up to 2 units of work in the provided monitor.
	 */
	private void fetchNewDirectory(Session session, RemoteFolderTree newFolder, String localPath, final IProgressMonitor monitor) throws CVSException {
		
		// Create an listener that will accumulate new files and folders
		IUpdateMessageListener listener = new IUpdateMessageListener() {
			public void directoryInformation(ICVSFolder root, String path, boolean newDirectory) {
				if (newDirectory) {
					// Record new directory with parent so it can be retrieved when building the parent
					// NOTE: Check path prefix
					recordDelta(path, FOLDER, Update.STATE_NONE);
					monitor.subTask(NLS.bind(CVSMessages.RemoteFolderTreeBuilder_receivingDelta, new String[] { Util.toTruncatedPath(path, 3) })); 
				}
			}
			public void directoryDoesNotExist(ICVSFolder root, String path) {
			}
			public void fileInformation(int type, ICVSFolder root, String filename) {
				// NOTE: Check path prefix
				changedFiles.add(filename);
				recordDelta(filename, ADDED, type);
				monitor.subTask(NLS.bind(CVSMessages.RemoteFolderTreeBuilder_receivingDelta, new String[] { Util.toTruncatedPath(filename, 3) })); 
			}
			public void fileDoesNotExist(ICVSFolder root, String filename) {
			}
		};

		// NOTE: Should use the path relative to the remoteRoot
		IStatus status = Command.UPDATE.execute(session,
			new GlobalOption[] { Command.DO_NOT_CHANGE },
			updateLocalOptions,
			new String[] { localPath },
			new UpdateListener(listener),
			Policy.subMonitorFor(monitor, 1)); 
		if (status.getCode() == CVSStatus.SERVER_ERROR) {
			CVSServerException e = new CVSServerException(status);
			if ( ! e.isNoTagException() && e.containsErrors())
				throw e;
			// we now know that this is an exception caused by a cvs bug.
			// if the folder has no files in it (just subfolders) cvs does not respond with the subfolders...
			// workaround: retry the request with no tag to get the directory names (if any)
			Policy.checkCanceled(monitor);
			status = Command.UPDATE.execute(session,
				new GlobalOption[] { Command.DO_NOT_CHANGE },
				getOptionsWithoutTag(),
				new String[] { localPath },
				new UpdateListener(listener),
				Policy.subMonitorFor(monitor, 1));
			if (status.getCode() == CVSStatus.SERVER_ERROR) {
				throw new CVSServerException(status);
			}
		}
	}
	
	// Get the file revisions for the given filenames
	private void fetchFileRevisions(Session session, String[] fileNames, final IProgressMonitor monitor) throws CVSException {
		
		// Create a listener for receiving the revision info
		final List<CVSException> exceptions = new ArrayList<CVSException>();
		IStatusListener listener = new IStatusListener() {
			public void fileStatus(ICVSFolder root, String path, String remoteRevision) {
				try {
					updateRevision(path, remoteRevision);
					monitor.subTask(NLS.bind(CVSMessages.RemoteFolderTreeBuilder_receivingRevision, new String[] { Util.toTruncatedPath(path, 3) })); 
				} catch (CVSException e) {
					exceptions.add(e);
				}
			}
		};
			
		// Perform a "cvs status..." with a custom message handler
		IStatus status = Command.STATUS.execute(session,
			Command.NO_GLOBAL_OPTIONS,
			Command.NO_LOCAL_OPTIONS,
			fileNames,
			new StatusListener(listener),
			monitor);
		if (status.getCode() == CVSStatus.SERVER_ERROR) {
			throw new CVSServerException(status);
		}
		
		// Report any exceptions that occurred fetching the revisions
		if ( ! exceptions.isEmpty()) {
			if (exceptions.size() == 1) {
				throw exceptions.get(0);
			} else {
				MultiStatus multi = new MultiStatus(CVSProviderPlugin.ID, 0, CVSMessages.RemoteFolder_errorFetchingRevisions, null); 
				for (int i = 0; i < exceptions.size(); i++) {
					multi.merge(exceptions.get(i).getStatus());
				}
				throw new CVSException(multi);
			}
		}
	}

	protected boolean isPruneEmptyDirectories() {
		return false;
	}
	/*
	 * Record the deltas in a double map where the outer key is the parent directory
	 * and the inner key is the file name. The value is the revision of the file or
	 * DELETED (file or folder). New folders have a revision of FOLDER.
	 * 
	 * A revision of UNKNOWN indicates that the revision has not been fetched
	 * from the repository yet.
	 */
	private void recordDelta(String path, String revision, int syncState) {
		if (revision == FOLDER) {
			newFolderExist = true;
		}
		String parent = Util.removeLastSegment(path);
		Map<String, DeltaNode> deltas = fileDeltas.get(parent);
		if (deltas == null) {
			deltas = new HashMap<>();
			fileDeltas.put(parent, deltas);
		}
		String name = Util.getLastSegment(path);
		deltas.put(name, new DeltaNode(name, revision, syncState));
	}
	
	private void updateRevision(String path, String revision) throws CVSException {
		RemoteFolderTree folder = getRecoredRemoteFolder(Util.removeLastSegment(path));
		if (folder == null) {
			IStatus status = new CVSStatus(IStatus.ERROR, CVSStatus.ERROR, NLS.bind(CVSMessages.RemoteFolderTreeBuilder_missingParent, new String[] { path.toString(), revision }), root);
			throw new CVSException(status);
		}
		((RemoteFile)folder.getFile(Util.getLastSegment(path))).setRevision(revision);
	}
	
	/*
	 * Return the tag that should be associated with a remote folder.
	 * 
	 * This method is used to ensure that new directories contain the tag
	 * derived from the parent local folder when appropriate. For instance,
	 * 
	 * The tag should be the provided tag. However, if tag is null, the 
	 * tag for the folder should be derived from the provided reference folder
	 * which could be the local resource corresponding to the remote or the parent
	 * of the remote.
	 */
	private CVSTag tagForRemoteFolder(ICVSFolder folder, CVSTag tag) throws CVSException {
		return tag == null ? folder.getFolderSyncInfo().getTag() : tag;
	}
	
	private boolean isOrphanedSubtree(ICVSFolder mFolder) throws CVSException {
		return mFolder.isCVSFolder() && ! mFolder.isManaged() && ! mFolder.equals(root) && mFolder.getParent().isCVSFolder();
	}
	
	private void recordRemoteFolder(RemoteFolderTree remote) throws CVSException {
		String path = remote.getFolderSyncInfo().getRemoteLocation();
		remoteFolderTable.put(Util.asPath(path), remote);
	}
	
	private RemoteFolderTree getRecoredRemoteFolder(String path) {
		return remoteFolderTable.get(Util.asPath(path));
	}

	/**
	 * This method returns an array of the files that differ between the local and remote trees.
	 * The files are represented as a String that contains the path to the file in the remote or local trees.
	 * @return an array of differing files
	 */
	public String[] getFileDiffs() {
		return changedFiles.toArray(new String[changedFiles.size()]);
	}
}

Back to the top