/ src / features / github-publishing / services / batch-share-service.ts
batch-share-service.ts
  1  import { Notice, Plugin } from 'obsidian';
  2  import { DreamNode } from '../../dreamnode';
  3  import { githubService } from './github-service';
  4  import { GitDreamNodeService } from '../../dreamnode/services/git-dreamnode-service';
  5  import { UDDService } from '../../dreamnode/services/udd-service';
  6  import { serviceManager } from '../../../core/services/service-manager';
  7  
  8  /**
  9   * GitHub Batch Share Service
 10   *
 11   * Ensures multiple DreamNodes have GitHub URLs before sharing via email links.
 12   * Handles batch GitHub share operations for Windows/GitHub fallback mode.
 13   */
 14  export class GitHubBatchShareService {
 15  	private plugin: Plugin;
 16  	private dreamNodeService: GitDreamNodeService;
 17  
 18  	constructor(plugin: Plugin, dreamNodeService: GitDreamNodeService) {
 19  		this.plugin = plugin;
 20  		this.dreamNodeService = dreamNodeService;
 21  	}
 22  
 23  	/**
 24  	 * Ensure all nodes have GitHub URLs, sharing those that don't
 25  	 * Returns map of UUID → GitHub URL
 26  	 */
 27  	async ensureNodesHaveGitHubUrls(nodeUUIDs: string[]): Promise<Map<string, string>> {
 28  		const result = new Map<string, string>();
 29  
 30  		if (nodeUUIDs.length === 0) {
 31  			return result;
 32  		}
 33  
 34  		try {
 35  			// Step 1: Load all nodes and check their GitHub status
 36  			const nodes: DreamNode[] = [];
 37  
 38  			for (const uuid of nodeUUIDs) {
 39  				const node = await this.dreamNodeService.get(uuid);
 40  				if (node) {
 41  					nodes.push(node);
 42  				} else {
 43  					console.warn(`[GitHubBatchShare] Node ${uuid} not found`);
 44  				}
 45  			}
 46  
 47  			// Step 2: Separate into already-shared vs needs-sharing
 48  			const { alreadyShared, needsSharing } = await this.categorizeNodes(nodes);
 49  
 50  			// Step 3: Add already-shared nodes to result
 51  			for (const node of alreadyShared) {
 52  				const githubUrl = await this.getGitHubUrlFromUdd(node);
 53  				if (githubUrl) {
 54  					result.set(node.id, githubUrl);
 55  				}
 56  			}
 57  
 58  			// Step 4: Batch share nodes that need it
 59  			if (needsSharing.length > 0) {
 60  				const notice = new Notice(`Sharing ${needsSharing.length} DreamNode${needsSharing.length > 1 ? 's' : ''} to GitHub...`, 0);
 61  
 62  				const shared = await this.batchShareNodes(needsSharing);
 63  
 64  				notice.hide();
 65  
 66  				// Add newly shared nodes to result
 67  				for (const [uuid, githubUrl] of shared) {
 68  					result.set(uuid, githubUrl);
 69  				}
 70  
 71  				const successCount = shared.size;
 72  				const failCount = needsSharing.length - successCount;
 73  
 74  				if (successCount > 0) {
 75  					new Notice(`Shared ${successCount} node${successCount > 1 ? 's' : ''} to GitHub`);
 76  				}
 77  
 78  				if (failCount > 0) {
 79  					console.warn(`[GitHubBatchShare] ${failCount} node(s) failed to share`);
 80  				}
 81  			}
 82  
 83  			return result;
 84  
 85  		} catch (error) {
 86  			console.error('[GitHubBatchShare] Batch sharing failed:', error);
 87  			throw error;
 88  		}
 89  	}
 90  
 91  	/**
 92  	 * Categorize nodes by GitHub sharing status
 93  	 */
 94  	private async categorizeNodes(nodes: DreamNode[]): Promise<{
 95  		alreadyShared: DreamNode[];
 96  		needsSharing: DreamNode[];
 97  	}> {
 98  		const alreadyShared: DreamNode[] = [];
 99  		const needsSharing: DreamNode[] = [];
100  
101  		for (const node of nodes) {
102  			const githubUrl = await this.getGitHubUrlFromUdd(node);
103  
104  			if (githubUrl) {
105  				alreadyShared.push(node);
106  			} else {
107  				needsSharing.push(node);
108  			}
109  		}
110  
111  		return { alreadyShared, needsSharing };
112  	}
113  
114  	/**
115  	 * Read GitHub URL from .udd file using UDDService
116  	 */
117  	private async getGitHubUrlFromUdd(node: DreamNode): Promise<string | null> {
118  		try {
119  			const vaultService = serviceManager.getVaultService();
120  			const fullRepoPath = vaultService?.getFullPath(node.repoPath) || node.repoPath;
121  
122  			const udd = await UDDService.readUDD(fullRepoPath);
123  			return (udd as any).githubRepoUrl || null;
124  		} catch {
125  			// Could not read .udd - this is expected for new nodes
126  			return null;
127  		}
128  	}
129  
130  	/**
131  	 * Batch share nodes to GitHub, serializing to prevent race conditions
132  	 * Returns map of successful UUID → GitHub URL
133  	 */
134  	private async batchShareNodes(nodes: DreamNode[]): Promise<Map<string, string>> {
135  		const result = new Map<string, string>();
136  
137  		// Check if GitHub is available
138  		const availabilityCheck = await githubService.isAvailable();
139  		if (!availabilityCheck.available) {
140  			throw new Error(availabilityCheck.error || 'GitHub CLI not available');
141  		}
142  
143  		// Get VaultService for path resolution
144  		const vaultService = serviceManager.getVaultService();
145  
146  		// CRITICAL: Serialize sharing to prevent race conditions
147  		// Process nodes one at a time to avoid git/GitHub conflicts
148  		for (const node of nodes) {
149  			try {
150  				const fullRepoPath = vaultService?.getFullPath(node.repoPath) || node.repoPath;
151  
152  				// Share to GitHub (creates repo, pushes, builds Pages)
153  				const shareResult = await githubService.shareDreamNode(
154  					fullRepoPath,
155  					node.id
156  				);
157  
158  				if (shareResult.repoUrl) {
159  					result.set(node.id, shareResult.repoUrl);
160  				} else {
161  					console.warn(`[GitHubBatchShare] ${node.name} shared but no GitHub URL returned`);
162  				}
163  
164  			} catch (error) {
165  				// Check if error is "already shared" or similar - this is NOT an error!
166  				const errorMsg = error instanceof Error ? error.message : String(error);
167  
168  				if (errorMsg.includes('already exists') || errorMsg.includes('already shared')) {
169  					try {
170  						const githubUrl = await this.getGitHubUrlFromUdd(node);
171  
172  						if (githubUrl) {
173  							result.set(node.id, githubUrl);
174  							continue; // Success! Move to next node
175  						}
176  					} catch (getUrlError) {
177  						console.error(`[GitHubBatchShare] Could not retrieve GitHub URL for ${node.name}:`, getUrlError);
178  					}
179  				} else {
180  					// Different error - log and continue
181  					console.error(`[GitHubBatchShare] Failed to share ${node.name}:`, error);
182  				}
183  			}
184  		}
185  
186  		return result;
187  	}
188  }
189  
190  // Singleton instance
191  let _githubBatchShareService: GitHubBatchShareService | null = null;
192  
193  export function initializeGitHubBatchShareService(plugin: Plugin, dreamNodeService: GitDreamNodeService): void {
194  	_githubBatchShareService = new GitHubBatchShareService(plugin, dreamNodeService);
195  }
196  
197  export function getGitHubBatchShareService(): GitHubBatchShareService {
198  	if (!_githubBatchShareService) {
199  		throw new Error('GitHubBatchShareService not initialized. Call initializeGitHubBatchShareService() first.');
200  	}
201  	return _githubBatchShareService;
202  }