Copy WebKit Permalink may generate wrong URL with SVN checkout
[WebKit-https.git] / Tools / CopyPermalink / Copy WebKit Permalink.workflow / Contents / document.wflow
1 <?xml version="1.0" encoding="UTF-8"?>
2 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 <plist version="1.0">
4 <dict>
5         <key>AMApplicationBuild</key>
6         <string>428</string>
7         <key>AMApplicationVersion</key>
8         <string>2.7</string>
9         <key>AMDocumentVersion</key>
10         <string>2</string>
11         <key>actions</key>
12         <array>
13                 <dict>
14                         <key>action</key>
15                         <dict>
16                                 <key>AMAccepts</key>
17                                 <dict>
18                                         <key>Container</key>
19                                         <string>List</string>
20                                         <key>Optional</key>
21                                         <true/>
22                                         <key>Types</key>
23                                         <array>
24                                                 <string>com.apple.applescript.object</string>
25                                         </array>
26                                 </dict>
27                                 <key>AMActionVersion</key>
28                                 <string>1.0</string>
29                                 <key>AMApplication</key>
30                                 <array>
31                                         <string>Automator</string>
32                                 </array>
33                                 <key>AMParameterProperties</key>
34                                 <dict>
35                                         <key>source</key>
36                                         <dict/>
37                                 </dict>
38                                 <key>AMProvides</key>
39                                 <dict>
40                                         <key>Container</key>
41                                         <string>List</string>
42                                         <key>Types</key>
43                                         <array>
44                                                 <string>com.apple.applescript.object</string>
45                                         </array>
46                                 </dict>
47                                 <key>ActionBundlePath</key>
48                                 <string>/System/Library/Automator/Run JavaScript.action</string>
49                                 <key>ActionName</key>
50                                 <string>Run JavaScript</string>
51                                 <key>ActionParameters</key>
52                                 <dict>
53                                         <key>source</key>
54                                         <string>/*
55  *  Copyright (C) 2017 Apple Inc. All rights reserved.
56  *
57  *  Redistribution and use in source and binary forms, with or without
58  *  modification, are permitted provided that the following conditions
59  *  are met:
60  *  1. Redistributions of source code must retain the above copyright
61  *     notice, this list of conditions and the following disclaimer.
62  *  2. Redistributions in binary form must reproduce the above copyright
63  *     notice, this list of conditions and the following disclaimer in the
64  *     documentation and/or other materials provided with the distribution.
65  *
66  *  THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
67  *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
68  *  THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
69  *  PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
70  *  BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
71  *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
72  *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
73  *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
74  *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
75  *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
76  *  THE POSSIBILITY OF SUCH DAMAGE.
77  */
78
79 ObjC.import("Cocoa");
80
81 var g_isSVN;
82 var g_isGit;
83 var g_isGitSVN;
84 var g_lastSVNInfo;
85
86 var App = Application.currentApplication();
87 App.includeStandardAdditions = true;
88
89 function run(input, parameters) {
90     var xcodeDocument = xcodeActiveDocument();
91     if (!xcodeDocument)
92         return;
93
94     var xcodeDocumentPath = xcodeDocument.path();
95     determineVCSFromPath(xcodeDocumentPath);
96
97     if (!pathIsInWebKitCheckout(xcodeDocumentPath))
98         return;
99
100     var lineNumber = xcodeSelectedLineInDocument(xcodeDocument);
101     var path = pathRelativeToRepositoryRootForPath(xcodeDocumentPath);
102     var revisionInfo = revisionInfoForPath(xcodeDocumentPath);
103     var annotateBlame = $.NSEvent.modifierFlags &amp; $.NSAlternateKeyMask;
104
105     App.setTheClipboardTo(permalinkForPath(path, lineNumber, revisionInfo, annotateBlame));
106 }
107
108 function pathIsInWebKitCheckout(path)
109 {
110     var repositoryURL = revisionInfoForPath(path).repositoryURL;
111     return !!repositoryURL.match(/^\w+:\/\/\w+\.webkit.org/);
112 }
113
114 function permalinkForPath(path, lineNumber, revisionInfo, annotateBlame)
115 {
116     var revision = revisionInfo.revision ? "?rev=" + revisionInfo.revision : "";
117     var lineNumber = lineNumber ? "#L" + lineNumber : "";
118     var branch = revisionInfo.branch || "trunk";
119     var withBlame = annotateBlame ? "&amp;annotate=blame" : "";
120     return `https://trac.webkit.org/browser/${branch}/${path}${revision}${withBlame}${lineNumber}`;
121 }
122
123 // MARK: Xcode
124
125 function xcodeActiveDocument()
126 {
127     var xcode = Application("Xcode");
128     var windows = xcode.windows();
129     var numberOfWindows = windows.length;
130     if (!numberOfWindows)
131         return null;
132
133     // The title of an Xcode Workspace window is the title of the document in the editor pane.
134     // Ignore windows without a name (e.g. "Edit all occurrences of a symbol" pop-up menu).
135     var documentName;
136     for (var i = 0; !documentName &amp;&amp; i &lt; numberOfWindows; ++i)
137         documentName = windows[i].name();
138     if (!documentName)
139         return null;
140
141     // The title of a modified document that has not been saved will have a suffix. Remove
142     // the suffix.
143     const editedSuffix = " — Edited";
144     if (documentName.endsWith(editedSuffix))
145         documentName = documentName.substr(0, documentName.lastIndexOf(editedSuffix));
146     return xcode.documents.byName(documentName);
147 }
148
149 function xcodeSelectedLineInDocument(xcodeDocument)
150 {
151     if (!xcodeDocument)
152         return -1;
153     var range = xcodeDocument.selectedCharacterRange();
154     if (!range)
155         return -1;
156     var beginPosition = range[0] - 1;
157     if (!beginPosition)
158         return 0;
159     // FIXME: It would be more efficient to count the CRLF, CR, or LF characters
160     // in the substring from [0, beginPosition].
161     var lines = xcodeDocument.text().split(/\r?\n|\r/);
162     var numberOfLines = lines.length;
163     var characterCount = 0;
164     var i = 0;
165     do {
166         characterCount += lines[i].length + 1;
167         if (characterCount &gt; beginPosition)
168             break;
169     } while (++i &lt; numberOfLines);
170     return i + 1;
171 }
172
173 // MARK: VCS utilities
174
175 function determineVCSFromPath(path)
176 {
177     if (!isDirectory(path))
178         path = dirname(path);
179
180     g_isSVN = false;
181     g_isGit = false;
182     g_isGitSVN = false;
183
184     if (isSVNDirectory(path)) {
185         g_isSVN = true;
186         return;
187     }
188
189     if (isGitSVNDirectory(path)) {
190         g_isGit = true;
191         g_isGitSVN = true;
192         return;
193     }
194
195     if (isGitDirectory(path)) {
196         g_isGit = true;
197         return;
198     }
199 }
200
201 function pathRelativeToRepositoryRootForPath(path)
202 {
203     var directoryInCheckout = isDirectory(path) ? path : dirname(path);
204     if (g_isSVN)
205         return svnPathRelativeToRepositoryRootForPath(path, directoryInCheckout);
206     if (g_isGit)
207         return gitPathRelativeToRepositoryRootForPath(path, directoryInCheckout);
208     return "";
209 }
210
211 function gitPathRelativeToRepositoryRootForPath(path, directoryInCheckout)
212 {
213     return App.doShellScript(`git -C '${directoryInCheckout}' ls-tree --full-name --name-only HEAD '${path}'`);
214 }
215
216 function svnPathRelativeToRepositoryRootForPath(path, directoryInCheckout)
217 {
218     return svnInfoForPath(path, directoryInCheckout).path;
219 }
220
221 function revisionInfoForPath(path)
222 {
223     var directoryInCheckout = isDirectory(path) ? path : dirname(path);
224     if (g_isSVN || g_isGitSVN)
225         return svnRevisionInfoForPath(path, directoryInCheckout);
226     if (g_isGit)
227         return gitRevisionInfoForPath(path, directoryInCheckout);
228     return "";
229 }
230
231 function svnRevisionInfoForPath(path, directoryInCheckout)
232 {
233     var svnInfo = svnInfoForPath(path, directoryInCheckout);
234     return { "branch": svnInfo.branch, "revision": svnInfo.revision, "repositoryURL": svnInfo.repositoryRoot };
235 }
236
237 function gitRevisionInfoForPath(path, directoryInCheckout)
238 {
239     var repositoryURL = App.doShellScript(`git -C '${directoryInCheckout}' remote get-url origin`);
240     var revision = App.doShellScript(`git -C '${directoryInCheckout}' log -1 --format='%H' '${path}'`);
241     var branch = App.doShellScript(`git -C '${directoryInCheckout}' symbolic-ref -q HEAD`);
242     branch = branch.replace(/^refs\/heads\//, "") || "master";
243     return { branch, revision, repositoryURL };
244 }
245
246 function svnInfoForPath(path, directoryInCheckout)
247 {
248     if (g_lastSVNInfo &amp;&amp; g_lastSVNInfo.path === path) {
249         // FIXME: We should also ensure that the checkout directory for the cached SVN info is
250         // the same as the specified checkout directory.
251         return g_lastSVNInfo;
252     }
253
254     var svnInfoCommand = "svn info";
255     if (g_isGitSVN)
256         svnInfoCommand = "git " + svnInfoCommand;
257     var output = App.doShellScript(`cd '${directoryInCheckout}' &amp;&amp; ${svnInfoCommand} '${path}'`, {"alteringLineEndings": false});
258     if (!output)
259         return { };
260
261     var temp = { };
262     var lines = output.split("\n");
263     for (var line of lines) {
264         var [key, value] = line.split(": ", 2);
265         if (key &amp;&amp; value)
266             temp[key] = value;
267     }
268     var svnInfo = {
269         "pathAsURL": temp["URL"],
270         "repositoryRoot": temp["Repository Root"],
271         "revision": temp["Revision"],
272     };
273     var branch = svnInfo.pathAsURL.replace(svnInfo.repositoryRoot + "/", "");
274     branch = branch.substr(0, branch.indexOf("/"));
275     svnInfo.branch = branch;
276
277     // Although tempting to use temp["Path"] we cannot because it is relative to directoryInCheckout.
278     // And directoryInCheckout may not be the top-level checkout directory. We need to compute the
279     // relative path with respect to the top-level checkout directory.
280     svnInfo.path = svnInfo.pathAsURL.replace(`${svnInfo.repositoryRoot}/${branch}/`, "");
281
282     g_lastSVNInfo = svnInfo;
283
284     return svnInfo;
285 }
286
287 function isSVNDirectory(directory)
288 {
289     try {
290         App.doShellScript(`cd '${directory}' &amp;&amp; svn info &gt; /dev/null 2&gt;&amp;1`);
291         return true;
292     } catch (e) {
293         return false;
294     }
295 }
296
297 function isGitDirectory(directory)
298 {
299     try {
300         App.doShellScript(`git -C '${directory}' rev-parse &gt; /dev/null 2&gt;&amp;1`);
301         return true;
302     } catch (e) {
303         return false;
304     }
305 }
306
307 function isGitSVNDirectory(directory)
308 {
309     var output = "";
310     try {
311         output = App.doShellScript(`git -C '${directory}' config --get svn-remote.svn.fetch 2&gt;&amp;1`);
312     } catch (e) { }
313     return output !== "";
314 }
315
316 // MARK: Utilities
317
318 function isDirectory(path)
319 {
320     try {
321         return App.infoFor(path).folder;
322     } catch (e) {
323         return false;
324     }
325 }
326
327 function dirname(path)
328 {
329     return path.substr(0, path.lastIndexOf("/"));
330 }
331 </string>
332                                 </dict>
333                                 <key>BundleIdentifier</key>
334                                 <string>com.apple.Automator.RunJavaScript</string>
335                                 <key>CFBundleVersion</key>
336                                 <string>1.0</string>
337                                 <key>CanShowSelectedItemsWhenRun</key>
338                                 <false/>
339                                 <key>CanShowWhenRun</key>
340                                 <true/>
341                                 <key>Category</key>
342                                 <array>
343                                         <string>AMCategoryUtilities</string>
344                                 </array>
345                                 <key>Class Name</key>
346                                 <string>RunJavaScriptAction</string>
347                                 <key>InputUUID</key>
348                                 <string>0C0655EF-7893-4A61-ADD0-BA803AF3C2CD</string>
349                                 <key>Keywords</key>
350                                 <array>
351                                         <string>Run</string>
352                                         <string>JavaScript</string>
353                                 </array>
354                                 <key>OutputUUID</key>
355                                 <string>5BAD8148-07E0-4FA2-AAA1-990A7BE926FC</string>
356                                 <key>UUID</key>
357                                 <string>24BFD6CC-7A96-42C2-8469-5D83FA921DB2</string>
358                                 <key>UnlocalizedApplications</key>
359                                 <array>
360                                         <string>Automator</string>
361                                 </array>
362                                 <key>arguments</key>
363                                 <dict>
364                                         <key>0</key>
365                                         <dict>
366                                                 <key>default value</key>
367                                                 <string>function run(input, parameters) {
368         
369         // Your script goes here
370
371         return input;
372 }</string>
373                                                 <key>name</key>
374                                                 <string>source</string>
375                                                 <key>required</key>
376                                                 <string>0</string>
377                                                 <key>type</key>
378                                                 <string>0</string>
379                                                 <key>uuid</key>
380                                                 <string>0</string>
381                                         </dict>
382                                 </dict>
383                                 <key>isViewVisible</key>
384                                 <true/>
385                                 <key>location</key>
386                                 <string>480.500000:316.000000</string>
387                                 <key>nibPath</key>
388                                 <string>/System/Library/Automator/Run JavaScript.action/Contents/Resources/Base.lproj/main.nib</string>
389                         </dict>
390                         <key>isViewVisible</key>
391                         <true/>
392                 </dict>
393         </array>
394         <key>connectors</key>
395         <dict/>
396         <key>workflowMetaData</key>
397         <dict>
398                 <key>serviceApplicationBundleID</key>
399                 <string>com.apple.dt.Xcode</string>
400                 <key>serviceApplicationPath</key>
401                 <string>/Applications/Xcode.app</string>
402                 <key>serviceInputTypeIdentifier</key>
403                 <string>com.apple.Automator.nothing</string>
404                 <key>serviceOutputTypeIdentifier</key>
405                 <string>com.apple.Automator.nothing</string>
406                 <key>serviceProcessesInput</key>
407                 <integer>0</integer>
408                 <key>workflowTypeIdentifier</key>
409                 <string>com.apple.Automator.servicesMenu</string>
410         </dict>
411 </dict>
412 </plist>