1 #title = "@VSS"
  2 #tooltip = "VSS操作"
  3 #include "lib\sgc4jsee.jsee"
  4 
  5 /**
  6  * @fileOverview
  7  * VSSを操作するための機能をポップアップメニューに表示し、選択された機能を実行します。<br />
  8  * 実務経験に基づき、出来るだけ安全に利用できるよう工夫されています。<br />
  9  * (共有ファイルを扱う際の注意喚起や、チェックイン前のDIFF確認、チェックアウト時の強制最新化など。)<br />
 10  * <p>
 11  * 同ディレクトリに「VSS操作.ini」が必要です。
 12  * </p>
 13  * <p>
 14  * <strong>INIファイルで比較ツールを指定することを強く推奨します。</strong><br />
 15  * 比較ツールは"diffTool.exe file1 file2"の形で起動されます。<br />
 16  * 推奨する比較ツールは<a href="http://www.forest.impress.co.jp/lib/stdy/program/progsupt/winmerge.html">WinMerge</a>です。
 17  * </p>
 18  * <p>
 19  * このマクロは、VSSの使用を推奨するものではありません。
 20  * </p>
 21  * <p>
 22  * 下記のサイトを参考にしました。
 23  * <ul>
 24  * <li><a href="http://d.hatena.ne.jp/Isoparametric/20100214/1266117253">Visual Source Safeを使用するのは狂気の沙汰 - 神様なんて信じない僕らのために</a></li>
 25  * <li><a href="http://d.hatena.ne.jp/Isoparametric/20100428/1272405617">VSSが相変わらずフルボッコすぎる件について - 神様なんて信じない僕らのために</a></li>
 26  * <li><a href="http://d.hatena.ne.jp/Isoparametric/20100928/1285657898">Visual Source Safeについてチェックしておきたい5つのTips - 神様なんて信じない僕らのために</a></li>
 27  * <li><a href="http://www.highprogrammer.com/alan/windev/sourcesafe.html">Visual SourceSafe: Microsoft's Source Destruction System</a></li>
 28  * <li><a href="http://togetter.com/li/4913">Togetter - 「VSSをDisりたい」</a></li>
 29  * <li><a href="http://anond.hatelabo.jp/20081129114129">VSSってこの世に何で存在してるの?</a></li>
 30  * <li><a href="http://blog.livedoor.jp/nobiru/archives/14263441.html">Nobiru's Blog:「Visual SourceSafe」の問題点</a></li>
 31  * <li><a href="http://blogs.wankuma.com/shuujin/archive/2008/03/15/127866.aspx">ここが変だよ、Visual SourceSafe のインターフェース</a></li>
 32  * </ul>
 33  * </p>
 34  * 
 35  * @author gecca from 雪月花 (http://setsugecca.org/)
 36  * @version 1.01 for EmEditor v10.0
 37  */
 38 
 39 (function() {
 40 	var logger = JseeUtil.getLogger(false, "VSS操作.jsee");
 41 	
 42 	var shell = JseeUtil.shell();
 43 	var fso = JseeUtil.fso();
 44 	
 45 	var iniFileName = "VSS操作.ini";
 46 	
 47 	var tempFileNamePrefix = "EmEditorMacros_VSS操作_コメント_";
 48 	var tempFileNameSuffix = ".tmp";
 49 	
 50 	var scriptName = "VSS操作";
 51 	
 52 	// INI情報を取得
 53 	var ini = getIni(iniFileName);
 54 	if(!ini) {
 55 		return;
 56 	}
 57 	
 58 	// ロジックを生成
 59 	var logic = new Logic(ini);
 60 	
 61 	// メニュー表示
 62 	JseeUtil.createPopupMenu([
 63 		["最新化 (&G)", logic.get],
 64 		["チェックアウト (&O)", logic.checkout],
 65 		["チェックイン (&I)", logic.checkin],
 66 		["履歴 (&H)", logic.history],
 67 		["比較 (&D)", logic.diff],
 68 		["分岐 (&B)", logic.branch],
 69 		["チェックアウトの取り消し (&U)", logic.undoCheckout],
 70 		["-", function() {}],
 71 		["最新を表示 (&V)", logic.view],
 72 		[(ini.diffToolName || "比較ツール") + "で最新と比較 (&W)", logic.diffTool],
 73 		["共有情報を表示 (&L)", logic.links],
 74 		["チェックアウト情報を表示 (&X)", logic.showCheckoutInfo],
 75 		["-", function() {}],
 76 		["チェックアウト中のファイルを探す (&A)", logic.searchDoingCheckout],
 77 		["-", function() {}],
 78 		["INIファイルを編集 (&E)", logic.openIniFile],
 79 		["users\\~\\ss.iniを編集 (&E)", logic.openSsIniFile],
 80 		["-", function() {}],
 81 		["ログイン情報をクリア (&C)", logic.clearLogin],
 82 	]);
 83 	
 84 	return;
 85 	
 86 	/**
 87 	 * INIファイルをオブジェクト化し、内容をチェックし、語尾の\や/を補完して、返却します。
 88 	 * 
 89 	 * @function
 90 	 * @param {String} iniFileName INIファイル名
 91 	 * @returns {Object} チェックがOKならINIオブジェクト、NGなら<code>undefined</code>
 92 	 * @see JseeUtil.getIniObj
 93 	 */
 94 	function getIni(iniFileName) {
 95 		// 生のINIオブジェクトを取得
 96 		var rawIni = JseeUtil.getIniObj(iniFileName);
 97 		logger.log("rawIni:" + JseeUtil.toString(rawIni));
 98 		
 99 		// エラー表示のためのエラー格納用配列
100 		var errorItems = [];
101 		var errors = [];
102 		if(!rawIni.ssDir) {
103 			errorItems.push("ssDir")
104 			errors.push("ssDirを設定してください。");
105 		} else if(!fso.FolderExists(rawIni.ssDir)) {
106 			errorItems.push("ssDir")
107 			errors.push("ssDirは存在しないフォルダです。");
108 		}
109 		if(!rawIni.vssDir) {
110 			errorItems.push("vssDir")
111 			errors.push("vssDirを設定してください。");
112 		} else if(!fso.FolderExists(rawIni.vssDir)) {
113 			errorItems.push("vssDir")
114 			errors.push("vssDirは存在しないフォルダです。");
115 		}
116 		if(!rawIni.workFolderOnExplorer) {
117 			errorItems.push("workFolderOnExplorer")
118 			errors.push("workFolderOnExplorerを設定してください。");
119 		} else if(!fso.FolderExists(rawIni.workFolderOnExplorer)) {
120 			errorItems.push("workFolderOnExplorer")
121 			errors.push("workFolderOnExplorerは存在しないフォルダです。");
122 		}
123 		if(!rawIni.workFolderOnVss) {
124 			errorItems.push("workFolderOnVss")
125 			errors.push("workFolderOnVssを設定してください。");
126 		} else if(rawIni.workFolderOnVss.indexOf("$") != 0) {
127 			errorItems.push("workFolderOnVss")
128 			errors.push("workFolderOnVssが$から始まっていません。");
129 		}
130 		// エラー格納用配列にエラーが入っていれば、エラー表示して、INIファイルを開いてエラー部分を強調して、処理を抜ける
131 		if(errors.length > 0) {
132 			alert(errors.join("\n"));
133 			JseeUtil.openFile(JseeUtil.getScriptPath() + iniFileName);
134 			document.selection.Find(errorItems.join("|") ,eeFindNext | eeFindReplaceRegExp);
135 			return;
136 		}
137 		
138 		// INIオブジェクトの語尾の\や/を補完
139 		var ini = rawIni;
140 		if(!/\\$/.test(ini.ssDir)) ini.ssDir += "\\";
141 		if(!/\\$/.test(ini.vssDir)) ini.vssDir += "\\";
142 		if(!/\\$/.test(ini.workFolderOnExplorer)) ini.workFolderOnExplorer += "\\";
143 		if(!/\/$/.test(ini.workFolderOnVss)) ini.workFolderOnVss += "/";
144 		
145 		logger.log("ini:" + JseeUtil.toString(ini));
146 		return ini;
147 	}
148 	
149 	/**
150 	 * VSSを操作するメソッド群を持つロジッククラスです。
151 	 * 
152 	 * @class
153 	 * @param {Object} ini INIファイルオブジェクト
154 	 */
155 	function Logic(ini) {
156 		// ss.exeのパスを作成
157 		var ssPath = "\"" + ini.vssDir + "ss.exe" + "\"";
158 		
159 		// 環境変数を設定
160 		(function() {
161 			// ダイアログからProcessスコープに設定
162 			if(!(shell.Environment("Process").Item("ssUser") && shell.Environment("Process").Item("ssPwd"))) {
163 				var input = prompt("ユーザ名とパスワードを\"user/pass\"の形式で入力してください。", "");
164 				if(!input) {
165 					Quit();
166 				}
167 				if(!/^([^\/]+)\/([^\/]+)$/.test(input)) {
168 					alert("\"user/pass\"の形式で入力してください。");
169 					Quit();
170 				}
171 				shell.Environment("Process").Item("ssUser") = RegExp.$1;
172 				shell.Environment("Process").Item("ssPwd") = RegExp.$2;
173 			}
174 			
175 			// INIファイルからProcessスコープに設定
176 			shell.Environment("Process").Item("ssDir") = ini.ssDir;
177 		})();
178 		
179 		// 対象ドキュメントのVSS上でのパス (『エクスプローラ上の「作業フォルダ」』の配下になければundefined) 
180 		var documentFullNameOnVss;
181 		
182 		init();
183 		
184 		/**
185 		 * {@link Logic}クラスが持つフィールドの内、開いているドキュメントに依存するものを初期化します。
186 		 * @function
187 		 */
188 		function init() {
189 			// 対象ドキュメントのVSS上でのパスを作成 (『エクスプローラ上の「作業フォルダ」』の配下になければundefined)
190 			if(document.FullName.indexOf(ini.workFolderOnExplorer) == 0) {
191 				documentFullNameOnVss = ini.workFolderOnVss + document.FullName.substring(ini.workFolderOnExplorer.length).replace(/\\/g, "/");
192 			}
193 			
194 			// カレントディレクトリをドキュメントのあるディレクトリに移動
195 			if(document.Path) {
196 				shell.CurrentDirectory = document.Path;
197 			}
198 		}
199 		
200 		/**
201 		 * VSSのコマンドを実行する共通処理です。
202 		 * 
203 		 * @function
204 		 * @param {String} command VSSコマンド
205 		 * @param {Function} [handleResult] 処理の実行結果を扱う関数。引数に右記の形のオブジェクトを取ります: {out: VSSコマンド実行時の標準出力, err: VSSコマンド実行時の標準エラー} (省略すれば非同期実行)
206 		 * @param {Function} [showConfirm] 確認ダイアログを出力したり処理を停止するための関数。処理を続行する場合は<code>true</code>を返す必要があります。 (省略可)
207 		 * @param {Function} [getComment] コメント(String型)を返す関数。第一引数に前回のコメントが渡ります。 (省略可)
208 		 * @param {Function} [getOption] オプション(String型)を返す関数。 (省略可)
209 		 */
210 		function execVssCommand(command, handleResult, showConfirm, getComment, getOption) {
211 			logger.log("command:" + command);
212 			
213 			// ドキュメントが対象となれるかどうかチェック
214 			if(command != "Whoami" && !documentFullNameOnVss) {
215 				alert("このドキュメントは、マクロ内に設定されている\n『エクスプローラ上の「作業フォルダ」』の配下にありません。\n\n何もせずに終了します。");
216 				Quit();
217 			}
218 			
219 			var tempFilePath = JseeUtil.getTempDirPath() + tempFileNamePrefix + command + tempFileNameSuffix;
220 			
221 			// コメント入力ダイアログを表示
222 			if(getComment) {
223 				var comment = getComment(JseeUtil.readFile(tempFilePath).replace(/\r\n|\r|\n/g, "\\n"));
224 			}
225 			
226 			// 確認ダイアログを表示
227 			if(showConfirm) {
228 				if(!showConfirm()) {
229 					Quit();
230 				};
231 			}
232 			
233 			// オプションの作成
234 			var option = "";
235 			
236 			// コメントの指定があれば、コメントのオプションを追加
237 			if(getComment) {
238 				JseeUtil.writeFile(tempFilePath, comment.replace(/\\r\\n|\\r|\\n/g, "\r\n"));
239 				option += " -C@" + tempFilePath + " ";
240 			}
241 			
242 			// 指定のオプションを追加
243 			if(getOption) {
244 				option += " " + getOption() + " ";
245 			}
246 			
247 			// コマンド実行
248 			var execPath = ssPath + " " + command + " " + (command != "Whoami" ? documentFullNameOnVss : "") + " " + option;
249 			logger.log("実行パス:" + execPath);
250 			
251 			if(handleResult) {
252 				var std = JseeUtil.exec(execPath);
253 				
254 				// 結果を取り扱う
255 				logger.log("std.out↓\n" + std.out);
256 				logger.log("std.err↓\n" + std.err);
257 				handleResult(std);
258 			} else {
259 				shell.Run(execPath, 0, true);
260 			}
261 		}
262 		
263 		/**
264 		 * VSSにログインしている自分のユーザ名を取得します。
265 		 * 
266 		 * @function
267 		 * @returns {String} VSSにログインしている自分のユーザ名
268 		 */
269 		function getMyName() {
270 			var myName;
271 			
272 			function handleResult(std) {
273 				if(std.err) {
274 					alert(std.err);
275 				} else {
276 					myName = std.out.match(/[^ \r\n]*/)[0];
277 				}
278 			};
279 			
280 			execVssCommand("Whoami", handleResult);
281 			
282 			return myName;
283 		}
284 		
285 		/**
286 		 * チェックアウトの情報を、{me: userName, others: userName}の形のオブジェクトで返します。<br />
287 		 * 自分がチェックアウトしていればme.userNameにユーザ名が、していなければ<code>undefined</code>が入ります。<br />他人がチェックアウトしている場合も同様です。
288 		 * 
289 		 * @function
290 		 * @returns {Object} {me: userName, others: userName}の形のオブジェクト
291 		 */
292 		function getCheckoutInfo() {
293 			var checkoutInfo = {me: undefined, others: undefined};
294 			
295 			function handleResult(std) {
296 				if(std.err) {
297 					alert(std.err);
298 				} else {
299 					// ユーザ名(+状態+時刻)にマッチ
300 					var m = std.out.match(/([^\s]+)\s*?[^\s]+\s*\d+\/\d+\/\d+/);
301 					if(m) {
302 						// マッチしたユーザ名が自分のユーザ名であれば、自分がチェックアウトしていると判断
303 						if(getMyName().indexOf(m[1]) == 0) {
304 							checkoutInfo.me = m[1];
305 						// マッチしたユーザ名が自分のユーザ名でなければ、他人がチェックアウトしていると判断
306 						} else {
307 							checkoutInfo.others = m[1];
308 						}
309 					}
310 				}
311 			};
312 			
313 			execVssCommand("Status", handleResult);
314 			
315 			return checkoutInfo;
316 		}
317 		
318 		/**
319 		 * 他のプロジェクトと共有している場合、共有しているプロジェクトへのパスを配列にして返します。<br />
320 		 * 共有していなければ<code>undefined</code>を返します。
321 		 * 
322 		 * @function
323 		 * @returns {Array} 他のプロジェクトと共有している場合は共有しているプロジェクトへのパスの配列、共有していなければ<code>undefined</code>
324 		 */
325 		function getLinks() {
326 			var linkPaths = [];
327 			
328 			function handleResult(std) {
329 				if(std.err) {
330 					alert(std.err);
331 				} else {
332 					var myDir = documentFullNameOnVss.replace(/\/[^\/]*$/, "");
333 					logger.log("myDir:" + myDir);
334 					
335 					// リンク先パスにマッチ
336 					var m = std.out.match(/^  \$[^\r\n]*$/gm);
337 					logger.log("m:" + m);
338 					if(m) {
339 						for(var i=0;i<m.length;i++) {
340 							var path = m[i].replace(/^  /, "");
341 							// 自分以外のプロジェクトであれば配列に追加
342 							if(path != myDir) {
343 								logger.log("path:" + path);
344 								linkPaths.push(path);
345 							}
346 						}
347 					}
348 					
349 					// リンク先パスにマッチ (2行に渡る場合)
350 					var m = std.out.match(/^\$[^\r\n]*(\r\n|\r|\n)[^\s>]+$/gm);
351 					logger.log("m:" + m);
352 					if(m) {
353 						alert("VSSから取得したリンク先のパスが途切れてしまっています!!\n正しいリンク先が取得できなかった可能性が高いです!!");
354 						for(var i=0;i<m.length;i++) {
355 							var path = m[i].replace(/^  /, "");
356 							// 自分以外のプロジェクトであれば配列に追加
357 							if(path != myDir) {
358 								logger.log("path:" + path);
359 								linkPaths.push(path);
360 							}
361 						}
362 					}
363 				}
364 			}
365 			
366 			execVssCommand("links", handleResult);
367 			
368 			// 他のプロジェクトと共有している場合のみ値を返す
369 			if(linkPaths.length) {
370 				return linkPaths;
371 			}
372 		};
373 		
374 		/**
375 		 * ss.iniファイルへのパスを取得します。
376 		 * @function
377 		 */
378 		function getSsIniPath() {
379 			return ini.ssDir + "users\\" + getMyName() + "\\ss.ini";
380 		}
381 		
382 		/**
383 		 * 開いているドキュメントと同名のファイルが同ディレクトリにあるかどうかチェックし、<br />
384 		 * 同名のファイルに同じ操作を行うかどうかのダイアログを表示します。
385 		 * 
386 		 * @function
387 		 * @param {Function} func 再度起動させるLogicクラスのメソッドオブジェクト
388 		 */
389 		var checkTheSameNameFile = (function() {
390 			// ドキュメント名のピリオドより前の部分
391 			var documentNameBeforePeriod = document.Name.replace(/\..+$/, "");
392 			
393 			// ドキュメントと同名のファイルが同じディレクトリにあれば、その拡張子(ピリオド以降全て)の配列を作成しておく
394 			var theSameNameFileExts = (function() {
395 				if(!document.FullName) {
396 					return [];
397 				}
398 				var exts = []
399 				var theSameNameMatcher = new RegExp(documentNameBeforePeriod + "\\.(.+)");
400 				var files = JseeUtil.col2arr(fso.GetFolder(document.Path).Files);
401 				var documentName = document.Name;
402 				for(var i=0;i<files.length;i++) {
403 					var file = files[i];
404 					if(file.Name != documentName && theSameNameMatcher.test(file.Name)) {
405 						exts.push(RegExp.$1);
406 					}
407 				}
408 				return exts;
409 			})();
410 			
411 			return function(func) {
412 				// 確認がキャンセルされれば別の候補で確認を続け、確認がOKされれば(次のメソッドがこの処理をやってくれるので)breakする
413 				while(theSameNameFileExts.length > 0) {
414 					var ext = theSameNameFileExts.pop();
415 					if(confirm("このファイルにはペアとなる" + ext + "ファイルがあります。\nこれを開いて同じ処理を行いますか?")) {
416 						JseeUtil.openFile(documentNameBeforePeriod + "." + ext);
417 						init();
418 						func();
419 						break;
420 					}
421 				}
422 			};
423 		})();
424 		
425 		/**
426 		 * VSSから最新ファイルを取得してドキュメントを上書きします。<br />
427 		 * チェックアウトしていれば処理を中断、読み取り専用が外れている場合は確認後に読み取り専用を付けて処理します。
428 		 * @function
429 		 */
430 		Logic.prototype.get = function() {
431 			if(getCheckoutInfo().me) {
432 				alert("既にチェックアウトしています。チェックアウトの取り消しを行ってください。");
433 				return;
434 			}
435 			function showConfirm() {
436 				if(!confirm("最新化してもよろしいですか?")) {
437 					return false;
438 				}
439 				
440 				var file = fso.GetFile(document.FullName);
441 				// 読み取り専用が外されていた場合
442 				if(!(file.Attributes & 1)) {
443 					if(!confirm("読み取り専用が解除されているため、\n読み取り専用に戻してから最新化します。\n\n処理を続行しますか?")) {
444 						Quit();
445 					}
446 					// 読み取り専用付加
447 					file.Attributes = (file.Attributes | 1);
448 				}
449 				
450 				return true;
451 			};
452 			
453 			function handleResult(std) {
454 				if(std.err) {
455 					alert(std.err);
456 				} else {
457 					alert("最新化しました。");
458 				}
459 			};
460 			
461 			execVssCommand("get", handleResult, showConfirm);
462 			checkTheSameNameFile(arguments.callee);
463 		};
464 		
465 		/**
466 		 * VSSからチェックアウトします。<br />
467 		 * チェックアウトしていれば処理を中断、共有リンクが張られている場合は警告を表示します。
468 		 * @function
469 		 */
470 		Logic.prototype.checkout = function() {
471 			var checkoutInfo = getCheckoutInfo();
472 			if(checkoutInfo.me) {
473 				alert("チェックアウト中です。");
474 				Quit();
475 			}
476 			if(checkoutInfo.others) {
477 				alert(checkoutInfo.others + "さんがチェックアウトしています。");
478 				Quit();
479 			}
480 			
481 			var links = getLinks();
482 			if(links) {
483 				alert("このファイルは\n\n" + links.join("\nと\n") + "\n\nに共有リンクが張られています。");
484 			}
485 			
486 			function showConfirm() {
487 				if(!confirm("最新化してドキュメントを読み直し、\nチェックアウトしてもよろしいですか?")) {
488 					return false;
489 				}
490 				
491 				// 最新化
492 				var succeededGet = (function() {
493 					var succeededGet = true;
494 					
495 					var file = fso.GetFile(document.FullName);
496 					// 読み取り専用が外されていた場合
497 					if(!(file.Attributes & 1)) {
498 						// 読み取り専用付加
499 						file.Attributes = (file.Attributes | 1);
500 					}
501 					
502 					function handleResult(std) {
503 						if(std.err) {
504 							alert(std.err);
505 							succeededGet = false;
506 						}
507 					}
508 					
509 					execVssCommand("get", handleResult);
510 					
511 					return succeededGet
512 				})();
513 				
514 				// 最新化が成功していれば処理を続行
515 				return succeededGet;
516 			};
517 			
518 			function getComment(prevComment) {
519 				return prompt("必要に応じてコメントを入力してください。 (改行は\\n)", prevComment);
520 			};
521 			
522 			function handleResult(std) {
523 				if(std.err) {
524 					alert(std.err);
525 				} else {
526 					alert("チェックアウトが完了しました。");
527 				}
528 			};
529 			
530 			execVssCommand("checkout", handleResult, showConfirm, getComment);
531 			// チェックアウト用の重要な独自処理
532 			(function() {
533 				// 再読み込み。これが無いと最新状態が再読み込みされないため非常に危険。
534 				editor.ExecuteCommandByID(4109);
535 				// チェックアウト後の読み取り専用状態を[書き換え禁止]に反映。これが無いとファイルシステムの状態変更が再読み込み時点に間に合わない。
536 				document.ReadOnly = fso.GetFile(document.FullName).Attributes & 1 != 0;
537 			})();
538 			checkTheSameNameFile(arguments.callee);
539 		};
540 		
541 		/**
542 		 * VSSからのチェックアウトを取り消します。<br />
543 		 * チェックアウトしていなければ処理を中断します。
544 		 * @function
545 		 */
546 		Logic.prototype.undoCheckout = function() {
547 			if(!getCheckoutInfo().me) {
548 				alert("チェックアウトしていません。");
549 				Quit();
550 			}
551 			
552 			function showConfirm() {
553 				return confirm("チェックアウトを取り消して\nローカルファイルを上書きしてもよろしいですか?");
554 			};
555 			
556 			function handleResult(std) {
557 				// 置換した時はstd.errにメッセージが入るため、Statusで判別する
558 				if(!getCheckoutInfo().me) {
559 					alert("チェックアウトを取り消しました。");
560 				} else {
561 					alert("チェックアウトの取り消しが失敗しました。");
562 				}
563 			};
564 			
565 			function getOption() {
566 				// ローカルファイルを上書きする
567 				return "-I-Y"
568 			};
569 			
570 			execVssCommand("UndoCheckout", handleResult, showConfirm, undefined, getOption);
571 			checkTheSameNameFile(arguments.callee);
572 		};
573 		
574 		/**
575 		 * VSSへチェックインします。
576 		 * チェックアウトしていなければ処理を中断、共有リンクが張られている場合は警告を表示します。<br />
577 		 * <br />
578 		 * 表示されるダイアログに答えることで、比較ツールで比較する機能に一旦移ることが出来ます。
579 		 * @function
580 		 */
581 		Logic.prototype.checkin = function() {
582 			if(!getCheckoutInfo().me) {
583 				alert("チェックアウトしていません。");
584 				Quit();
585 			}
586 			
587 			var links = getLinks();
588 			if(links) {
589 				alert("このファイルは\n\n" + links.join("\nと\n") + "\n\nに共有リンクが張られています。");
590 			}
591 			
592 			if(confirm("チェックインする前に" + (ini.diffToolName || "比較ツール") + "で比較しますか?\n\n(比較中にファイルが変更された場合は読み直します。)")) {
593 				// 更新日時を保持
594 				var dateLastModified = String(fso.GetFile(document.FullName).DateLastModified);
595 				if(!Logic.prototype.diffTool(true)) {
596 					alert("チェックインを中止しました。");
597 					return;
598 				}
599 				// 更新日時が変わっていれば読み直す
600 				if(dateLastModified != String(fso.GetFile(document.FullName).DateLastModified)) {
601 					editor.ExecuteCommandByID(4109);
602 				}
603 			}
604 			
605 			function showConfirm() {
606 				return confirm("チェックインしてもよろしいですか?");
607 			};
608 			function getComment(prevComment) {
609 				return prompt("必要に応じてコメントを入力してください。 (改行は\\n)", prevComment);
610 			};
611 			function getOption() {
612 				// 読み取り専用にする
613 				return "-W-";
614 			};
615 			
616 			function handleResult(std) {
617 				if(std.err) {
618 					alert(std.err);
619 				} else {
620 					alert("チェックインが完了しました。");
621 				}
622 			};
623 			
624 			var std = execVssCommand("checkin", handleResult, showConfirm, getComment, getOption);
625 			checkTheSameNameFile(arguments.callee);
626 		};
627 		
628 		/**
629 		 * VSSから履歴を取得し、整形してアウトプットバーに表示します。
630 		 * @function
631 		 */
632 		Logic.prototype.history = function() {
633 			function showConfirm() {
634 				return confirm("アウトプットバーに履歴を表示してもよろしいですか?");
635 			};
636 			
637 			function handleResult(std) {
638 				if(std.err) {
639 					alert(std.err);
640 				} else {
641 					var logger = JseeUtil.getLogger(true, "");
642 					logger.log(std.out
643 						.replace(/(\r\n|\r)/g, "\n")
644 						.replace(/\*\*\*.*バージョン (\d+).*\n/g, (function() {
645 							var digit;
646 							return function($0, $1) {
647 								if(!digit) digit = $1.length;
648 								var buf = $1;
649 								for(;digit>buf.length;buf = "0" + buf);
650 								return "[" + buf + "]";
651 							};
652 						})())
653 						.replace(/(時刻:\s*\d+:\d+\n).*\n/g, "$1")
654 						.replace(/ユーザー: (.*?)\s*日付:\s*(\d+\/\d+\/\d+)\s*時刻:\s*(\d+:\d+)/g, "$2 $3 $1")
655 						.replace(/コメント: \n/g, "")
656 						.replace(/コメント: /g, "")
657 					);
658 				}
659 			};
660 			
661 			var std = execVssCommand("history", handleResult, showConfirm);
662 		};
663 		
664 		/**
665 		 * VSSの最新リビジョンと比較し、結果を整形してアウトプットバーに表示します。
666 		 * @function
667 		 */
668 		Logic.prototype.diff = function() {
669 			function showConfirm() {
670 				return confirm("アウトプットバーに比較結果を表示します。\n相違点が多いと時間がかかる可能性があります。\n\n処理を続けてもよろしいですか?");
671 			};
672 			function getOption() {
673 				// 標準形式で一行に150文字表示
674 				return "-DS150";
675 			};
676 			
677 			function handleResult(std) {
678 				if(std.err) {
679 					alert(std.err);
680 				} else {
681 					var logger = JseeUtil.getLogger(true, "");
682 					logger.log(std.out);
683 				}
684 			};
685 			
686 			execVssCommand("diff", handleResult, showConfirm, undefined, getOption);
687 		};
688 		
689 		/**
690 		 * 分岐します。
691 		 * @function
692 		 */
693 		Logic.prototype.branch = function() {
694 			var links = getLinks();
695 			if(!links) {
696 				alert("共有リンクは張られていません。");
697 				return;
698 			}
699 			
700 			function showConfirm() {
701 				return confirm("分岐してもよろしいですか?");
702 			}
703 			
704 			function getComment(prevComment) {
705 				return prompt("必要に応じてコメントを入力してください。 (改行は\\n)", prevComment);
706 			}
707 			
708 			function handleResult(std) {
709 				if(std.err) {
710 					alert(std.err);
711 				} else {
712 					alert("分岐しました。");
713 				}
714 			}
715 			
716 			execVssCommand("branch", handleResult, showConfirm, getComment);
717 		};
718 		
719 		/**
720 		 * 最新リビジョンをテンポラリ領域に取得し、それを開きます。<br />
721 		 * ss.iniにEditorが指定されている必要があります。<br />
722 		 * <p>
723 		 * マクロと同じプロセスでは動作しない、かつ標準入力を利用しないので、{@link Logic.execVssCommand}を利用しない実装となっています。
724 		 * </p>
725 		 * 
726 		 * @function
727 		 * @param {Boolean} checkEmEditor ss.iniのEditorがEmEditorかどうかチェックする場合は<code>true</code>、それ以外は省略
728 		 * @returns {Boolean} 成功すれば<code>true</code>、それ以外はundefined
729 		 */
730 		Logic.prototype.view = function(checkEmEditor) {
731 			// エディタ設定のチェック
732 			var ssIniPath = getSsIniPath();
733 			if(!fso.FileExists(ssIniPath)) {
734 				alert("以下の場所にss.iniが見つかりませんでした。\n\n" + ssIniPath);
735 				return;
736 			}
737 			if(!JseeUtil.getIniObj(ssIniPath).Editor) {
738 				alert("ss.iniにEditorの設定がない、または記述の場所が違います。\n\nss.iniの冒頭に、\nEditor=C:\\PROGRA~1\\EmEditor\\EmEditor.exe\nのような記述を追加してください。\n\nss.iniを開きます。");
739 				JseeUtil.openFile(ssIniPath);
740 				return;
741 			}
742 			if(checkEmEditor) {
743 				if(JseeUtil.getIniObj(ssIniPath).Editor.indexOf("EmEditor.exe") < 0) {
744 					alert("この機能はss.iniのEditor設定がEmEditorでなければ使えません。\nss.iniを開きます。");
745 					JseeUtil.openFile(ssIniPath);
746 					document.selection.EndOfDocument(false);
747 					document.selection.Find("Editor\s*=\s*.*",eeFindPrevious | eeFindReplaceCase | eeFindAround | eeFindReplaceRegExp);
748 					return;
749 				}
750 			}
751 			logger.log(JseeUtil.getIniObj(ssIniPath));
752 			
753 			// コマンド実行
754 			execVssCommand("view");
755 			
756 			return true;
757 		};
758 		
759 		/**
760 		 * 最新リビジョンをテンポラリ領域に取得し、それと現在のファイルを比較ツールで比較します。<br />
761 		 * 比較ツールは"diffTool.exe file1.txt file2.txt"の形で起動できるものに限られます。<br />
762 		 * INIファイルのdiffToolPathを指定する必要があります。<br />
763 		 * <br />
764 		 * ss.iniのEditorキーにEmEditorのパスが指定されている必要があります。
765 		 * 
766 		 * @function
767 		 * @param {Boolean} [waitToolExit] 比較ツールの終了を待つなら<code>true</code> (省略可能)
768 		 * @returns {Boolean} 正常に処理を抜ければ<code>true</code>
769 		 */
770 		Logic.prototype.diffTool = function(waitToolExit) {
771 			if(!ini.diffToolPath) {
772 				alert("INIファイルにdiffToolPathを指定しないと、この機能は使えません。");
773 				JseeUtil.openFile(JseeUtil.getScriptPath() + iniFileName);
774 				return;
775 			}
776 			if(!fso.FileExists(ini.diffToolPath)) {
777 				alert("INIファイルのdiffToolPathに指定されたファイルパスが存在しません。");
778 				JseeUtil.openFile(JseeUtil.getScriptPath() + iniFileName, "^diffToolPath");
779 				return;
780 			}
781 			
782 			var doc = document;
783 			var docsCnt = editor.Documents.Count;
784 			
785 			// 取得
786 			if(!Logic.prototype.view(true)) {
787 				return;
788 			};
789 			
790 			// 比較ツールを起動
791 			if(!waitToolExit) {
792 				shell.Run("\"" + ini.diffToolPath + "\" \"" + doc.FullName + "\" \"" + document.FullName + "\"", 0, false);
793 			} else {
794 				JseeUtil.exec("\"" + ini.diffToolPath + "\" \"" + doc.FullName + "\" \"" + document.FullName + "\"");
795 			}
796 			
797 			// 新しいドキュメントが開かれていれば、それを閉じて元のドキュメントをアクティブ化
798 			if(docsCnt != editor.Documents.Count) {
799 				document.Close();
800 				doc.Activate();
801 			}
802 			
803 			return true;
804 		};
805 		
806 		/**
807 		 * チェックアウト情報を表示します。
808 		 * @function
809 		 */
810 		Logic.prototype.showCheckoutInfo = function() {
811 			var checkoutInfo = getCheckoutInfo();
812 			if(checkoutInfo.me) {
813 				alert("チェックアウト中です。");
814 			} else if(checkoutInfo.others) {
815 				alert(checkoutInfo.others + "さんがチェックアウト中です。");
816 			} else {
817 				alert("誰もチェックアウトしていません。");
818 			}
819 		};
820 		
821 		Logic.prototype.showAllMyCheckoutInfo = function() {
822 			execVssCommand("Status", handleResult);
823 		};
824 		
825 		/**
826 		 * 共有情報を表示します。
827 		 * @function
828 		 */
829 		Logic.prototype.links = function() {
830 			var links = getLinks();
831 			if(links) {
832 				alert("このファイルは\n\n" + links.join("\nと\n") + "\n\nに共有リンクが張られています。");
833 			} else {
834 				alert("このファイルに共有リンクは張られていません。");
835 			}
836 		};
837 		
838 		/**
839 		 * チェックアウト中のファイルを探して表示します。
840 		 * @function
841 		 */
842 		Logic.prototype.searchDoingCheckout = function() {
843 			if(!confirm("チェックアウト中のファイルを探して表示します。\n\n調査対象パス: \"" + ini.workFolderOnVss + "\"\n\nこの処理は異常に時間がかかる可能性があるため非同期で実行されます。\n実行が完了した段階でファイルを開いて結果を表示します。\n\n処理を続行しますか?")) {
844 				return;
845 			}
846 			
847 			// 実行結果ハンドラ
848 			var resultHandle = function(result) {
849 				var fso = new ActiveXObject("Scripting.FileSystemObject");
850 				
851 				// 結果メッセージが空白の場合は処理が中断されたと判断
852 				if(!result) {
853 					WScript.Echo("処理が中断されました。");
854 					return;
855 				}
856 				
857 				// チェックアウトしていない場合はそのまま返却
858 				if(result.indexOf("によってチェックアウトされているファイルは見つかりません") >= 0) {
859 					return ("■調査対象パス\n"
860 						+ "%ini.workFolderOnVss%\n"
861 						+ "\n"
862 						+ "■チェクアウト中\n"
863 						+ "なし\n"
864 					).replace(/\n/g, "\r\n");
865 				}
866 				
867 				var filePaths = result.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(new RegExp("[^\n]+\n([^\n]+?) +%getMyName% +[^\\s]+? +\\d+\\/\\d+\\/\\d+ \\d+:\\d+ *\n([^\n]+)(\n\n|\n*$)", "g"), "$2\\$1\n").split(/\n/);
868 				// 各ファイルパスの存在をチェックし、存在しなければ補完処理を行う
869 				var correctFilePaths = [];
870 				var incorrectFilePaths = [];
871 				for(var i=0;i<filePaths.length;i++) {
872 					var filePath = filePaths[i];
873 					if(fso.FileExists(filePath)) {
874 						// ファイルが存在すれば正規パスとして採用
875 						correctFilePaths.push(filePath);
876 					} else {
877 						// ファイルが存在しなければ、ss.exeがファイル名を途切れさせたものと見て、ディレクトリから一致するファイル名を探して補完
878 						if(/(.+\\)([^\\]+)$/.test(filePath)) {
879 							var dirPath = RegExp.$1;
880 							var halfFileName = RegExp.$2;
881 							
882 							var candidateFiles = [];
883 							for(var e = new Enumerator(fso.GetFolder(dirPath).Files);!e.atEnd();e.moveNext()) candidateFiles.push(e.item());
884 							var candidateFileNamesStr = "";
885 							for(var j=0;j<candidateFiles.length;j++) {
886 								candidateFileNamesStr += "|" + candidateFiles[j].Name;
887 							}
888 							var foundFullFileNames = candidateFileNamesStr.match(new RegExp("\\|" + halfFileName + "[^|]*", "g"));
889 							if(foundFullFileNames.length != 1) {
890 								// Not Found or Too Match
891 								incorrectFilePaths.push(filePath);
892 							} else {
893 								// 該当ファイルが1つだけ見つかった場合はそのファイルパスを設定
894 								correctFilePaths.push(dirPath + foundFullFileNames[0].replace(/^\|/g, ""));
895 							}
896 						} else {
897 							// ここには入らないはずだが、一応フォロー
898 							incorrectFilePaths.push(filePath);
899 						}
900 					}
901 				}
902 				
903 				// ファイル出力内容を作成
904 				var fileContent = 
905 					"■調査対象パス\n"
906 					+ "%ini.workFolderOnVss%\n"
907 					+ "\n"
908 					+ "■チェクアウト中\n"
909 					+ (correctFilePaths.length ? correctFilePaths.join("\n") : "なし") + "\n"
910 				;
911 				if(incorrectFilePaths.length) {
912 					fileContent += "\n"
913 						+ "■チェクアウト中 (VSSのss.exeから得たファイル名が途切れていて、ファイル名の補完も出来なかったパス)\n"
914 						+ incorrectFilePaths.join("\n") + "\n";
915 				}
916 				
917 				// 改行コードの統一
918 				return fileContent.replace(/\n/g, "\r\n");
919 			};
920 			
921 			execAsync(ssPath + " status " + ini.workFolderOnVss + " -R -U",
922 				resultHandle.toString().replace(/%ini.workFolderOnVss%/g, ini.workFolderOnVss).replace(/%getMyName%/g, getMyName()));
923 		};
924 		
925 		/**
926 		 * Userスコープの環境変数に保存したログイン情報を削除します。
927 		 * @function
928 		 */
929 		Logic.prototype.clearLogin = function() {
930 			shell.Environment("User").Remove("ssUser");
931 			shell.Environment("User").Remove("ssPwd");
932 			alert("ユーザ環境変数に保存していたログイン情報をクリアしました。\nマクロを再実行すると、再度ログイン情報を設定できます。");
933 		};
934 		
935 		/**
936 		 * マクロのINIファイルを開きます。
937 		 * @function
938 		 */
939 		Logic.prototype.openIniFile = function() {
940 			JseeUtil.openFile(JseeUtil.getScriptPath() + iniFileName);
941 		};
942 		
943 		/**
944 		 * ss.iniファイルを開きます。
945 		 * @function
946 		 */
947 		Logic.prototype.openSsIniFile = function() {
948 			JseeUtil.openFile(getSsIniPath());
949 		};
950 	}
951 	
952 	function execAsync(execSentence, handleResult) {
953 		var scriptName = ScriptName.replace(/\.[^.]*$/, "");
954 		
955 		// 引数チェック
956 		if(typeof handleResult != "undefined" && typeof handleResult != "function" && typeof handleResult != "string") {
957 			throw new Error("handleResultが関数または文字列ではなく、" + typeof handleResult + "です。");
958 		}
959 		
960 		var shell = JseeUtil.shell();
961 		var fso = JseeUtil.fso();
962 		
963 		logger.log("実行パス:" + execSentence);
964 		
965 		// WSHファイルパス
966 		var wshFilePath = JseeUtil.getTempDirPath() + scriptName + "_" + JseeUtil.dateFormat(new Date(), "yyyyMMddHHmmssSSS") + ".js";
967 		
968 		// 結果ファイルパス
969 		var resultFilePath = JseeUtil.getTempDirPath() + scriptName + "_" + JseeUtil.dateFormat(new Date(), "yyyyMMddHHmmssSSS") + ".tmp";
970 		logger.log("resultFilePath:" + resultFilePath);
971 		// ファイル名として使えるかどうか事前にチェックしておく
972 		try {
973 			fso.CreateTextFile(resultFilePath).Close();
974 		} catch(e) {
975 			throw new Error("execSentenceの冒頭で宣言されている以下のコマンドが、\nファイルパスとして使用できませんでした。\n\n" + execSentenceCommand);
976 		}
977 		
978 		// WSHファイル内容を作成
979 		var source = "";
980 		if(handleResult) {
981 			source += "var handleResult = " + handleResult + ";\n"
982 		}
983 		source += "var execSentence = \"" + execSentence.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"") + "\";\n"
984 			+ "var emFilePath = \"" + editor.FullName.replace(/\\/g, "\\\\") + "\";\n"
985 			+ "var resultFilePath = \"" + resultFilePath.replace(/\\/g, "\\\\") + "\";\n"
986 			+ "var wshFilePath = \"" + wshFilePath.replace(/\\/g, "\\\\") + "\";\n"
987 			+ "var popupTitle = \"" + scriptName + "\";\n"
988 			+ "\n"
989 			+ (function() {
990 				try {
991 					var shell = new ActiveXObject("WScript.Shell");
992 					var fso = new ActiveXObject("Scripting.FileSystemObject");
993 					
994 					try {
995 						var execed = shell.exec(execSentence);
996 					} catch(e) {
997 						// 「指定されたファイルが見つかりません。」の場合
998 						if(e.message.match(/指定されたファイルが見つかりません。\r\n/)) {
999 							WScript.Echo("以下のコマンドを実行しようとしましたが失敗しました。\r\nパスに空白が入っている場合は\"\"で囲ってください。\r\n\r\n" + execSentence);
1000 							WScript.Quit();
1001 						// それ以外の場合
1002 						} else {
1003 							throw e;
1004 						}
1005 					}
1006 					
1007 					var result = "";
1008 					while(!execed.StdOut.AtEndOfStream) {
1009 						result += execed.StdOut.Read(102400);
1010 					}
1011 					while(!execed.StdErr.AtEndOfStream) {
1012 						result += execed.StdErr.Read(102400);
1013 					}
1014 					if(handleResult) {
1015 						result = handleResult(result);
1016 						if(typeof result == "undefined") {
1017 							return;
1018 						}
1019 					}
1020 					
1021 					// 実行結果ファイルに実行結果を出力
1022 					var date = new Date();
1023 					var stream = fso.OpenTextFile(resultFilePath, 2, true);
1024 					stream.Write(result);
1025 					stream.Close();
1026 					
1027 					// 実行結果ファイルを開く
1028 					if(shell.Popup("非同期処理が完了しました。結果を開いてよろしいですか?\r\n\r\n実行したコマンド:" + execSentence, 0, popupTitle, 1) != 2) {
1029 						shell.Run("\"" + emFilePath + "\" \"" + resultFilePath + "\"");
1030 					}
1031 					
1032 					WScript.Sleep(1000);
1033 					// 実行結果ファイルを削除
1034 					fso.DeleteFile(resultFilePath);
1035 					// 自分を削除
1036 					fso.DeleteFile(wshFilePath);
1037 				} catch(e) {
1038 					WScript.Echo("処理が失敗しました。実行したWSHソースを開きます。");
1039 					shell.Run("\"" + emFilePath + "\" \"" + wshFilePath + "\"");
1040 					throw e;
1041 				}
1042 			}).toString() + "();"
1043 		;
1044 		// WSHソースの改行コードを統一
1045 		source = source.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n");
1046 		
1047 		logger.log("WSHソース:" + source);
1048 		
1049 		// WSHファイル出力
1050 		JseeUtil.writeFile(wshFilePath, source);
1051 		
1052 		// WSHファイル実行
1053 		shell.Run(wshFilePath, 0, false);
1054 	}
1055 })();
1056