WebKit Bugzilla
Attachment 349679 Details for
Bug 189589
: Add Copy WebKit Permalink plugin for Sublime Text
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Requests
|
Help
|
New Account
|
Log In
Remember
[x]
|
Forgot Password
Login:
[x]
[patch]
Patch
bug-189589-20180913103913.patch (text/plain), 42.81 KB, created by
Daniel Bates
on 2018-09-13 10:39:14 PDT
(
hide
)
Description:
Patch
Filename:
MIME Type:
Creator:
Daniel Bates
Created:
2018-09-13 10:39:14 PDT
Size:
42.81 KB
patch
obsolete
>Subversion Revision: 235974 >diff --git a/Tools/ChangeLog b/Tools/ChangeLog >index 076c830ca61be30339b1cf5d2f484b1cd0d5ae97..1a858473b638d9d77f583c9e4010ec451c0807f1 100644 >--- a/Tools/ChangeLog >+++ b/Tools/ChangeLog >@@ -1,3 +1,28 @@ >+2018-09-13 Daniel Bates <dabates@apple.com> >+ >+ Add Copy WebKit Permalink plugin for Sublime Text >+ https://bugs.webkit.org/show_bug.cgi?id=189589 >+ >+ Reviewed by NOBODY (OOPS!). >+ >+ Port the Copy WebKit Permalink Automator service to a Sublime Text plugin. Once installed you can >+ use the plugin to copy to the Clipboard a trac.webkit.org hyperlink to the selected line in the >+ active document with or without blame annotations. >+ >+ Once installed, you can Control-click (on Mac) or right-click (on Windows and Linux) on a line and >+ choose Copy WebKit Permalink or Copy WebKit Permalink to Blame from the context menu to copy to the >+ Clipboard a permanent hyperlink to the selected line without or with blame annotations, respectively. >+ On Mac you can also invoke the same functionality using the keyboard shortcuts Command + Shift + Control + C >+ and Command + Shift + Control + Option + C, respectively. >+ >+ * CopyPermalink/Sublime Text/CopyWebKitPermalink/Context.sublime-menu: Added. >+ * CopyPermalink/Sublime Text/CopyWebKitPermalink/CopyWebKitPermalink.py: Added. >+ * CopyPermalink/Sublime Text/CopyWebKitPermalink/Default (OSX).sublime-keymap: Added. >+ * CopyPermalink/Sublime Text/INSTALL: Added. >+ * CopyPermalink/Xcode/Copy WebKit Permalink.workflow/Contents/Info.plist: Renamed from Tools/CopyPermalink/Copy WebKit Permalink.workflow/Contents/Info.plist. >+ * CopyPermalink/Xcode/Copy WebKit Permalink.workflow/Contents/document.wflow: Renamed from Tools/CopyPermalink/Copy WebKit Permalink.workflow/Contents/document.wflow. >+ * CopyPermalink/Xcode/INSTALL: Renamed from Tools/CopyPermalink/README. >+ > 2018-09-13 Carlos Garcia Campos <cgarcia@igalia.com> > > Unreviewed. Fix WebDriver tests after r235225. >diff --git a/Tools/CopyPermalink/Copy WebKit Permalink.workflow/Contents/Info.plist b/Tools/CopyPermalink/Copy WebKit Permalink.workflow/Contents/Info.plist >deleted file mode 100644 >index 59d26a4c8c43cf09b0fb04731c15903a35f2358c..0000000000000000000000000000000000000000 >--- a/Tools/CopyPermalink/Copy WebKit Permalink.workflow/Contents/Info.plist >+++ /dev/null >@@ -1,23 +0,0 @@ >-<?xml version="1.0" encoding="UTF-8"?> >-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> >-<plist version="1.0"> >-<dict> >- <key>NSServices</key> >- <array> >- <dict> >- <key>NSMenuItem</key> >- <dict> >- <key>default</key> >- <string>Copy WebKit Permalink</string> >- </dict> >- <key>NSMessage</key> >- <string>runWorkflowAsService</string> >- <key>NSRequiredContext</key> >- <dict> >- <key>NSApplicationIdentifier</key> >- <string>com.apple.dt.Xcode</string> >- </dict> >- </dict> >- </array> >-</dict> >-</plist> >diff --git a/Tools/CopyPermalink/Copy WebKit Permalink.workflow/Contents/document.wflow b/Tools/CopyPermalink/Copy WebKit Permalink.workflow/Contents/document.wflow >deleted file mode 100644 >index 7368c18729f0858ebc5377a9a637ccbbef3fb5a7..0000000000000000000000000000000000000000 >--- a/Tools/CopyPermalink/Copy WebKit Permalink.workflow/Contents/document.wflow >+++ /dev/null >@@ -1,412 +0,0 @@ >-<?xml version="1.0" encoding="UTF-8"?> >-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> >-<plist version="1.0"> >-<dict> >- <key>AMApplicationBuild</key> >- <string>428</string> >- <key>AMApplicationVersion</key> >- <string>2.7</string> >- <key>AMDocumentVersion</key> >- <string>2</string> >- <key>actions</key> >- <array> >- <dict> >- <key>action</key> >- <dict> >- <key>AMAccepts</key> >- <dict> >- <key>Container</key> >- <string>List</string> >- <key>Optional</key> >- <true/> >- <key>Types</key> >- <array> >- <string>com.apple.applescript.object</string> >- </array> >- </dict> >- <key>AMActionVersion</key> >- <string>1.0</string> >- <key>AMApplication</key> >- <array> >- <string>Automator</string> >- </array> >- <key>AMParameterProperties</key> >- <dict> >- <key>source</key> >- <dict/> >- </dict> >- <key>AMProvides</key> >- <dict> >- <key>Container</key> >- <string>List</string> >- <key>Types</key> >- <array> >- <string>com.apple.applescript.object</string> >- </array> >- </dict> >- <key>ActionBundlePath</key> >- <string>/System/Library/Automator/Run JavaScript.action</string> >- <key>ActionName</key> >- <string>Run JavaScript</string> >- <key>ActionParameters</key> >- <dict> >- <key>source</key> >- <string>/* >- * Copyright (C) 2017 Apple Inc. All rights reserved. >- * >- * Redistribution and use in source and binary forms, with or without >- * modification, are permitted provided that the following conditions >- * are met: >- * 1. Redistributions of source code must retain the above copyright >- * notice, this list of conditions and the following disclaimer. >- * 2. Redistributions in binary form must reproduce the above copyright >- * notice, this list of conditions and the following disclaimer in the >- * documentation and/or other materials provided with the distribution. >- * >- * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' >- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, >- * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR >- * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS >- * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR >- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF >- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS >- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN >- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) >- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF >- * THE POSSIBILITY OF SUCH DAMAGE. >- */ >- >-ObjC.import("Cocoa"); >- >-var g_isSVN; >-var g_isGit; >-var g_isGitSVN; >-var g_lastSVNInfo; >- >-var App = Application.currentApplication(); >-App.includeStandardAdditions = true; >- >-function run(input, parameters) { >- var xcodeDocument = xcodeActiveDocument(); >- if (!xcodeDocument) >- return; >- >- var xcodeDocumentPath = xcodeDocument.path(); >- determineVCSFromPath(xcodeDocumentPath); >- >- if (!pathIsInWebKitCheckout(xcodeDocumentPath)) >- return; >- >- var lineNumber = xcodeSelectedLineInDocument(xcodeDocument); >- var path = pathRelativeToRepositoryRootForPath(xcodeDocumentPath); >- var revisionInfo = revisionInfoForPath(xcodeDocumentPath); >- var annotateBlame = $.NSEvent.modifierFlags & $.NSAlternateKeyMask; >- >- App.setTheClipboardTo(permalinkForPath(path, lineNumber, revisionInfo, annotateBlame)); >-} >- >-function pathIsInWebKitCheckout(path) >-{ >- var repositoryURL = revisionInfoForPath(path).repositoryURL; >- return !!repositoryURL.match(/^\w+:\/\/\w+\.webkit.org/); >-} >- >-function permalinkForPath(path, lineNumber, revisionInfo, annotateBlame) >-{ >- var revision = revisionInfo.revision ? "?rev=" + revisionInfo.revision : ""; >- var lineNumber = lineNumber ? "#L" + lineNumber : ""; >- var branch = revisionInfo.branch || "trunk"; >- var withBlame = annotateBlame ? "&annotate=blame" : ""; >- return `https://trac.webkit.org/browser/${branch}/${path}${revision}${withBlame}${lineNumber}`; >-} >- >-// MARK: Xcode >- >-function xcodeActiveDocument() >-{ >- var xcode = Application("Xcode"); >- var windows = xcode.windows(); >- var numberOfWindows = windows.length; >- if (!numberOfWindows) >- return null; >- >- // The title of an Xcode Workspace window is the title of the document in the editor pane. >- // Ignore windows without a name (e.g. "Edit all occurrences of a symbol" pop-up menu). >- var documentName; >- for (var i = 0; !documentName && i < numberOfWindows; ++i) >- documentName = windows[i].name(); >- if (!documentName) >- return null; >- >- // The title of a modified document that has not been saved will have a suffix. Remove >- // the suffix. >- const editedSuffix = " â Edited"; >- if (documentName.endsWith(editedSuffix)) >- documentName = documentName.substr(0, documentName.lastIndexOf(editedSuffix)); >- return xcode.documents.byName(documentName); >-} >- >-function xcodeSelectedLineInDocument(xcodeDocument) >-{ >- if (!xcodeDocument) >- return -1; >- var range = xcodeDocument.selectedCharacterRange(); >- if (!range) >- return -1; >- var beginPosition = range[0] - 1; >- if (!beginPosition) >- return 0; >- // FIXME: It would be more efficient to count the CRLF, CR, or LF characters >- // in the substring from [0, beginPosition]. >- var lines = xcodeDocument.text().split(/\r?\n|\r/); >- var numberOfLines = lines.length; >- var characterCount = 0; >- var i = 0; >- do { >- characterCount += lines[i].length + 1; >- if (characterCount > beginPosition) >- break; >- } while (++i < numberOfLines); >- return i + 1; >-} >- >-// MARK: VCS utilities >- >-function determineVCSFromPath(path) >-{ >- if (!isDirectory(path)) >- path = dirname(path); >- >- g_isSVN = false; >- g_isGit = false; >- g_isGitSVN = false; >- >- if (isSVNDirectory(path)) { >- g_isSVN = true; >- return; >- } >- >- if (isGitSVNDirectory(path)) { >- g_isGit = true; >- g_isGitSVN = true; >- return; >- } >- >- if (isGitDirectory(path)) { >- g_isGit = true; >- return; >- } >-} >- >-function pathRelativeToRepositoryRootForPath(path) >-{ >- var directoryInCheckout = isDirectory(path) ? path : dirname(path); >- if (g_isSVN) >- return svnPathRelativeToRepositoryRootForPath(path, directoryInCheckout); >- if (g_isGit) >- return gitPathRelativeToRepositoryRootForPath(path, directoryInCheckout); >- return ""; >-} >- >-function gitPathRelativeToRepositoryRootForPath(path, directoryInCheckout) >-{ >- return App.doShellScript(`git -C '${directoryInCheckout}' ls-tree --full-name --name-only HEAD '${path}'`); >-} >- >-function svnPathRelativeToRepositoryRootForPath(path, directoryInCheckout) >-{ >- return svnInfoForPath(path, directoryInCheckout).path; >-} >- >-function revisionInfoForPath(path) >-{ >- var directoryInCheckout = isDirectory(path) ? path : dirname(path); >- if (g_isSVN || g_isGitSVN) >- return svnRevisionInfoForPath(path, directoryInCheckout); >- if (g_isGit) >- return gitRevisionInfoForPath(path, directoryInCheckout); >- return ""; >-} >- >-function svnRevisionInfoForPath(path, directoryInCheckout) >-{ >- var svnInfo = svnInfoForPath(path, directoryInCheckout); >- return { "branch": svnInfo.branch, "revision": svnInfo.revision, "repositoryURL": svnInfo.repositoryRoot }; >-} >- >-function gitRevisionInfoForPath(path, directoryInCheckout) >-{ >- var repositoryURL = App.doShellScript(`git -C '${directoryInCheckout}' remote get-url origin`); >- var revision = App.doShellScript(`git -C '${directoryInCheckout}' log -1 --format='%H' '${path}'`); >- var branch = App.doShellScript(`git -C '${directoryInCheckout}' symbolic-ref -q HEAD`); >- branch = branch.replace(/^refs\/heads\//, "") || "master"; >- return { branch, revision, repositoryURL }; >-} >- >-function svnInfoForPath(path, directoryInCheckout) >-{ >- if (g_lastSVNInfo && g_lastSVNInfo.path === path) { >- // FIXME: We should also ensure that the checkout directory for the cached SVN info is >- // the same as the specified checkout directory. >- return g_lastSVNInfo; >- } >- >- var svnInfoCommand = "svn info"; >- if (g_isGitSVN) >- svnInfoCommand = "git " + svnInfoCommand; >- var output = App.doShellScript(`cd '${directoryInCheckout}' && ${svnInfoCommand} '${path}'`, {"alteringLineEndings": false}); >- if (!output) >- return { }; >- >- var temp = { }; >- var lines = output.split("\n"); >- for (var line of lines) { >- var [key, value] = line.split(": ", 2); >- if (key && value) >- temp[key] = value; >- } >- var svnInfo = { >- "pathAsURL": temp["URL"], >- "repositoryRoot": temp["Repository Root"], >- "revision": temp["Revision"], >- }; >- var branch = svnInfo.pathAsURL.replace(svnInfo.repositoryRoot + "/", ""); >- branch = branch.substr(0, branch.indexOf("/")); >- svnInfo.branch = branch; >- >- // Although tempting to use temp["Path"] we cannot because it is relative to directoryInCheckout. >- // And directoryInCheckout may not be the top-level checkout directory. We need to compute the >- // relative path with respect to the top-level checkout directory. >- svnInfo.path = svnInfo.pathAsURL.replace(`${svnInfo.repositoryRoot}/${branch}/`, ""); >- >- g_lastSVNInfo = svnInfo; >- >- return svnInfo; >-} >- >-function isSVNDirectory(directory) >-{ >- try { >- App.doShellScript(`cd '${directory}' && svn info > /dev/null 2>&1`); >- return true; >- } catch (e) { >- return false; >- } >-} >- >-function isGitDirectory(directory) >-{ >- try { >- App.doShellScript(`git -C '${directory}' rev-parse > /dev/null 2>&1`); >- return true; >- } catch (e) { >- return false; >- } >-} >- >-function isGitSVNDirectory(directory) >-{ >- var output = ""; >- try { >- output = App.doShellScript(`git -C '${directory}' config --get svn-remote.svn.fetch 2>&1`); >- } catch (e) { } >- return output !== ""; >-} >- >-// MARK: Utilities >- >-function isDirectory(path) >-{ >- try { >- return App.infoFor(path).folder; >- } catch (e) { >- return false; >- } >-} >- >-function dirname(path) >-{ >- return path.substr(0, path.lastIndexOf("/")); >-} >-</string> >- </dict> >- <key>BundleIdentifier</key> >- <string>com.apple.Automator.RunJavaScript</string> >- <key>CFBundleVersion</key> >- <string>1.0</string> >- <key>CanShowSelectedItemsWhenRun</key> >- <false/> >- <key>CanShowWhenRun</key> >- <true/> >- <key>Category</key> >- <array> >- <string>AMCategoryUtilities</string> >- </array> >- <key>Class Name</key> >- <string>RunJavaScriptAction</string> >- <key>InputUUID</key> >- <string>0C0655EF-7893-4A61-ADD0-BA803AF3C2CD</string> >- <key>Keywords</key> >- <array> >- <string>Run</string> >- <string>JavaScript</string> >- </array> >- <key>OutputUUID</key> >- <string>5BAD8148-07E0-4FA2-AAA1-990A7BE926FC</string> >- <key>UUID</key> >- <string>24BFD6CC-7A96-42C2-8469-5D83FA921DB2</string> >- <key>UnlocalizedApplications</key> >- <array> >- <string>Automator</string> >- </array> >- <key>arguments</key> >- <dict> >- <key>0</key> >- <dict> >- <key>default value</key> >- <string>function run(input, parameters) { >- >- // Your script goes here >- >- return input; >-}</string> >- <key>name</key> >- <string>source</string> >- <key>required</key> >- <string>0</string> >- <key>type</key> >- <string>0</string> >- <key>uuid</key> >- <string>0</string> >- </dict> >- </dict> >- <key>isViewVisible</key> >- <true/> >- <key>location</key> >- <string>480.500000:316.000000</string> >- <key>nibPath</key> >- <string>/System/Library/Automator/Run JavaScript.action/Contents/Resources/Base.lproj/main.nib</string> >- </dict> >- <key>isViewVisible</key> >- <true/> >- </dict> >- </array> >- <key>connectors</key> >- <dict/> >- <key>workflowMetaData</key> >- <dict> >- <key>serviceApplicationBundleID</key> >- <string>com.apple.dt.Xcode</string> >- <key>serviceApplicationPath</key> >- <string>/Applications/Xcode.app</string> >- <key>serviceInputTypeIdentifier</key> >- <string>com.apple.Automator.nothing</string> >- <key>serviceOutputTypeIdentifier</key> >- <string>com.apple.Automator.nothing</string> >- <key>serviceProcessesInput</key> >- <integer>0</integer> >- <key>workflowTypeIdentifier</key> >- <string>com.apple.Automator.servicesMenu</string> >- </dict> >-</dict> >-</plist> >diff --git a/Tools/CopyPermalink/README b/Tools/CopyPermalink/README >deleted file mode 100644 >index 6358c1f52d8b45eb9d45dcc9a0afa3b0c130c307..0000000000000000000000000000000000000000 >--- a/Tools/CopyPermalink/README >+++ /dev/null >@@ -1,18 +0,0 @@ >-Copy WebKit Permalink is an Xcode service that copies to the Clipboard a permanent hyperlink to the currently selected line in the active Xcode document. >- >-To install: >- >-1. Double-click "Copy WebKit Permalink.workflow". >-2. In the dialog that appears, click Install, then click Done. >-3. Choose Apple menu > System Preferences, click Keyboard, then click Shortcuts. >-4. Select Services on the left, select Copy WebKit Permalink, click in the Keyboard Shortcut field, then press the key combination that you want to use as the keyboard shortcut. >- >-For example, press Command, Shift, Control, and C keys at the same time. >- >-5. Enable "Copy WebKit Permalink" using the checkbox. >- >-The Copy WebKit Permalink service will now appear under Xcode menu > Services. >- >-== Permalink to blame history == >- >-Hold down the Option key when running the Copy WebKit Permalink service to generate a permalink to the selected line in the blame history for the file. >diff --git a/Tools/CopyPermalink/Sublime Text/CopyWebKitPermalink/Context.sublime-menu b/Tools/CopyPermalink/Sublime Text/CopyWebKitPermalink/Context.sublime-menu >new file mode 100644 >index 0000000000000000000000000000000000000000..6b07953ccdf12b55c34e4bae0c87e32a7c8c0936 >--- /dev/null >+++ b/Tools/CopyPermalink/Sublime Text/CopyWebKitPermalink/Context.sublime-menu >@@ -0,0 +1,4 @@ >+[ >+ { "command": "copy_web_kit_permalink" }, >+ { "command": "copy_web_kit_permalink", "args": { "annotate_blame": true }, "caption": "Copy WebKit Permalink to Blame" }, >+] >diff --git a/Tools/CopyPermalink/Sublime Text/CopyWebKitPermalink/CopyWebKitPermalink.py b/Tools/CopyPermalink/Sublime Text/CopyWebKitPermalink/CopyWebKitPermalink.py >new file mode 100644 >index 0000000000000000000000000000000000000000..ef1beeba5e595ea7a4c529059800a005b0a67874 >--- /dev/null >+++ b/Tools/CopyPermalink/Sublime Text/CopyWebKitPermalink/CopyWebKitPermalink.py >@@ -0,0 +1,178 @@ >+# Copyright (C) 2018 Apple Inc. All rights reserved. >+# >+# Redistribution and use in source and binary forms, with or without >+# modification, are permitted provided that the following conditions >+# are met: >+# 1. Redistributions of source code must retain the above copyright >+# notice, this list of conditions and the following disclaimer. >+# 2. Redistributions in binary form must reproduce the above copyright >+# notice, this list of conditions and the following disclaimer in the >+# documentation and/or other materials provided with the distribution. >+# >+# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND >+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED >+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE >+# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR >+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL >+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR >+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER >+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, >+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE >+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. >+ >+import os >+import re >+import sublime >+import sublime_plugin >+import subprocess >+ >+ >+class CopyWebKitPermalinkCommand(sublime_plugin.TextCommand): >+ def run(self, edit, annotate_blame=False): >+ if not self.is_enabled(): >+ return >+ >+ document_path = self.view.file_name() >+ self._last_svn_info = None >+ self._directory_in_checkout = document_path if os.path.isdir(document_path) else os.path.dirname(document_path) >+ self.determine_vcs_from_path(document_path) >+ >+ if not self.path_is_in_webkit_checkout(document_path): >+ return >+ >+ line_number, _ = self.view.rowcol(self.view.sel()[0].begin()) # Zero-based >+ line_number = line_number + 1 >+ >+ path = self.path_relative_to_repository_root_for_path(document_path) >+ revision_info = self.revision_info_for_path(document_path) >+ sublime.set_clipboard(self.permalink_for_path(path, line_number, revision_info, annotate_blame)) >+ >+ def is_enabled(self): >+ return len(self.view.sel()) > 0 and bool(self.view.file_name()) >+ >+ def is_visible(self): >+ return self.is_enabled() >+ >+ def description(self): >+ return 'Copy WebKit Permalink' >+ >+ def determine_vcs_from_path(self, path): >+ if not os.path.isdir(path): >+ path = os.path.dirname(path) >+ self._is_svn = False >+ self._is_git = False >+ self._is_git_svn = False >+ if self.is_svn_directory(path): >+ self._is_svn = True >+ return >+ if self.is_git_svn_directory(path): >+ self._is_git = True >+ self._is_git_svn = True >+ return >+ if self.is_git_directory(path): >+ self._is_git = True >+ return >+ >+ def path_is_in_webkit_checkout(self, path): >+ repository_url = self.revision_info_for_path(path).get('repository_url', '') >+ return bool(re.match(r'\w+:\/\/\w+\.webkit.org', repository_url)) >+ >+ def git_path_relative_to_repository_root_for_path(self, path): >+ return subprocess.check_output(['git', 'ls-tree', '--full-name', '--name-only', 'HEAD', path], cwd=self._directory_in_checkout).decode('utf-8').rstrip() >+ >+ def svn_path_relative_to_repository_root_for_path(self, path): >+ return self.svn_info_for_path(path)['path'] >+ >+ def path_relative_to_repository_root_for_path(self, path): >+ if self._is_svn: >+ return self.svn_path_relative_to_repository_root_for_path(path) >+ if self._is_git: >+ return self.git_path_relative_to_repository_root_for_path(path) >+ return '' >+ >+ def revision_info_for_path(self, path): >+ if self._is_svn or self._is_git_svn: >+ return self.svn_revision_info_for_path(path) >+ if self._is_git: >+ return self.git_revision_info_for_path(path) >+ return {} >+ >+ def svn_revision_info_for_path(self, path): >+ svn_info = self.svn_info_for_path(path) >+ return {'branch': svn_info['branch'], 'revision': svn_info['revision'], 'repository_url': svn_info['repositoryRoot']} >+ >+ def git_revision_info_for_path(self, path): >+ repository_url = subprocess.check_output(['git', 'remote', 'get-url', 'origin'], cwd=self._directory_in_checkout).decode('utf-8').rstrip() >+ revision = subprocess.check_output(['git', 'log', '-1', '--format', '%H', path], cwd=self._directory_in_checkout).decode('utf-8').rstrip() >+ branch = subprocess.check_output(['git', 'symbolic-ref', '-q', 'HEAD'], cwd=self._directory_in_checkout).decode('utf-8').rstrip() >+ branch = re.sub(r'^refs\/heads\/', '', branch) or 'master' >+ return {branch, revision, repository_url} >+ >+ def svn_info_for_path(self, path): >+ if self._last_svn_info and self._last_svn_info['path'] == path: >+ # FIXME: We should also ensure that the checkout directory for the cached SVN info is >+ # the same as the specified checkout directory. >+ return self._last_svn_info >+ >+ svn_info_command = ['svn', 'info'] >+ if self._is_git_svn: >+ svn_info_command = ['git'] + svn_info_command >+ output = subprocess.check_output(svn_info_command + [path], cwd=self._directory_in_checkout).decode('utf-8').rstrip() >+ if not output: >+ return {} >+ >+ temp = {} >+ lines = output.splitlines() >+ for line in lines: >+ key, value = line.split(': ', 1) >+ if key and value: >+ temp[key] = value >+ >+ svn_info = { >+ 'pathAsURL': temp['URL'], >+ 'repositoryRoot': temp['Repository Root'], >+ 'revision': temp['Revision'], >+ } >+ branch = svn_info['pathAsURL'].replace(svn_info['repositoryRoot'] + '/', '') >+ branch = branch[0:branch.find('/')] >+ svn_info['branch'] = branch >+ >+ # Although tempting to use temp['Path'] we cannot because it is relative to self._directory_in_checkout. >+ # And self._directory_in_checkout may not be the top-level checkout directory. We need to compute the >+ # relative path with respect to the top-level checkout directory. >+ svn_info['path'] = svn_info['pathAsURL'].replace('{}/{}/'.format(svn_info['repositoryRoot'], branch), '') >+ >+ self._last_svn_info = svn_info >+ >+ return svn_info >+ >+ @staticmethod >+ def permalink_for_path(path, line_number, revision_info, annotate_blame): >+ revision = '?rev=' + str(revision_info['revision']) if revision_info['revision'] else '' >+ line_number = '#L' + str(line_number) if line_number else '' >+ branch = revision_info['branch'] or 'trunk' >+ annotate_blame = '&annotate=blame' if annotate_blame else '' >+ return 'https://trac.webkit.org/browser/{}/{}{}{}{}'.format(branch, path, revision, annotate_blame, line_number) >+ >+ @staticmethod >+ def is_svn_directory(directory): >+ try: >+ subprocess.check_call(['svn', 'info'], cwd=directory) >+ except: >+ return False >+ return True >+ >+ @staticmethod >+ def is_git_directory(directory): >+ try: >+ subprocess.check_call(['git', 'rev-parse'], cwd=directory) >+ except: >+ return False >+ return True >+ >+ @staticmethod >+ def is_git_svn_directory(directory): >+ try: >+ return bool(subprocess.check_output(['git', 'config', '--get', 'svn-remote.svn.fetch'], cwd=directory, stderr=subprocess.STDOUT).decode('utf-8').rstrip()) >+ except: >+ return False >diff --git a/Tools/CopyPermalink/Sublime Text/CopyWebKitPermalink/Default (OSX).sublime-keymap b/Tools/CopyPermalink/Sublime Text/CopyWebKitPermalink/Default (OSX).sublime-keymap >new file mode 100644 >index 0000000000000000000000000000000000000000..c08b06b6f8fc0328734b27386c57c536cf56be63 >--- /dev/null >+++ b/Tools/CopyPermalink/Sublime Text/CopyWebKitPermalink/Default (OSX).sublime-keymap >@@ -0,0 +1,4 @@ >+[ >+ { "keys": ["super+ctrl+shift+c"], "command": "copy_web_kit_permalink" }, >+ { "keys": ["super+ctrl+option+shift+c"], "command": "copy_web_kit_permalink", "args": { "annotate_blame": true } } >+] >diff --git a/Tools/CopyPermalink/Sublime Text/INSTALL b/Tools/CopyPermalink/Sublime Text/INSTALL >new file mode 100644 >index 0000000000000000000000000000000000000000..427f1ffb565bc6bdd64b407d885c1998716589d3 >--- /dev/null >+++ b/Tools/CopyPermalink/Sublime Text/INSTALL >@@ -0,0 +1,14 @@ >+Copy WebKit Permalink is a Sublime Text plugin that copies to the Clipboard a permanent hyperlink to the currently selected line in the active document. >+ >+== How to install == >+ >+Copy the directory CopyWebKitPermalink (located in the same directory as this file) to your Packages directory. On Mac, you can open your Packages directory by launching Sublime Text and choosing Sublime Text > Preferences > Browse Packages from the menu bar. >+ >+== How to use == >+ >+Control-click (on Mac) or right-click (on Linux and Windows) on a line and select Copy WebKit Permalink or Copy WebKit Permalink to Blame from the context menu to copy to the Clipboard a permanent hyperlink to the selected line without or with blame annotations, respectively. >+ >+== Key bindings for Mac == >+ >+Without blame annotations: Command + Shift + Control + C >+With blame annotations: Command + Shift + Control + Option + C >diff --git a/Tools/CopyPermalink/Xcode/Copy WebKit Permalink.workflow/Contents/Info.plist b/Tools/CopyPermalink/Xcode/Copy WebKit Permalink.workflow/Contents/Info.plist >new file mode 100644 >index 0000000000000000000000000000000000000000..59d26a4c8c43cf09b0fb04731c15903a35f2358c >--- /dev/null >+++ b/Tools/CopyPermalink/Xcode/Copy WebKit Permalink.workflow/Contents/Info.plist >@@ -0,0 +1,23 @@ >+<?xml version="1.0" encoding="UTF-8"?> >+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> >+<plist version="1.0"> >+<dict> >+ <key>NSServices</key> >+ <array> >+ <dict> >+ <key>NSMenuItem</key> >+ <dict> >+ <key>default</key> >+ <string>Copy WebKit Permalink</string> >+ </dict> >+ <key>NSMessage</key> >+ <string>runWorkflowAsService</string> >+ <key>NSRequiredContext</key> >+ <dict> >+ <key>NSApplicationIdentifier</key> >+ <string>com.apple.dt.Xcode</string> >+ </dict> >+ </dict> >+ </array> >+</dict> >+</plist> >diff --git a/Tools/CopyPermalink/Xcode/Copy WebKit Permalink.workflow/Contents/document.wflow b/Tools/CopyPermalink/Xcode/Copy WebKit Permalink.workflow/Contents/document.wflow >new file mode 100644 >index 0000000000000000000000000000000000000000..7368c18729f0858ebc5377a9a637ccbbef3fb5a7 >--- /dev/null >+++ b/Tools/CopyPermalink/Xcode/Copy WebKit Permalink.workflow/Contents/document.wflow >@@ -0,0 +1,412 @@ >+<?xml version="1.0" encoding="UTF-8"?> >+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> >+<plist version="1.0"> >+<dict> >+ <key>AMApplicationBuild</key> >+ <string>428</string> >+ <key>AMApplicationVersion</key> >+ <string>2.7</string> >+ <key>AMDocumentVersion</key> >+ <string>2</string> >+ <key>actions</key> >+ <array> >+ <dict> >+ <key>action</key> >+ <dict> >+ <key>AMAccepts</key> >+ <dict> >+ <key>Container</key> >+ <string>List</string> >+ <key>Optional</key> >+ <true/> >+ <key>Types</key> >+ <array> >+ <string>com.apple.applescript.object</string> >+ </array> >+ </dict> >+ <key>AMActionVersion</key> >+ <string>1.0</string> >+ <key>AMApplication</key> >+ <array> >+ <string>Automator</string> >+ </array> >+ <key>AMParameterProperties</key> >+ <dict> >+ <key>source</key> >+ <dict/> >+ </dict> >+ <key>AMProvides</key> >+ <dict> >+ <key>Container</key> >+ <string>List</string> >+ <key>Types</key> >+ <array> >+ <string>com.apple.applescript.object</string> >+ </array> >+ </dict> >+ <key>ActionBundlePath</key> >+ <string>/System/Library/Automator/Run JavaScript.action</string> >+ <key>ActionName</key> >+ <string>Run JavaScript</string> >+ <key>ActionParameters</key> >+ <dict> >+ <key>source</key> >+ <string>/* >+ * Copyright (C) 2017 Apple Inc. All rights reserved. >+ * >+ * Redistribution and use in source and binary forms, with or without >+ * modification, are permitted provided that the following conditions >+ * are met: >+ * 1. Redistributions of source code must retain the above copyright >+ * notice, this list of conditions and the following disclaimer. >+ * 2. Redistributions in binary form must reproduce the above copyright >+ * notice, this list of conditions and the following disclaimer in the >+ * documentation and/or other materials provided with the distribution. >+ * >+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' >+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, >+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR >+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS >+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR >+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF >+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS >+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN >+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) >+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF >+ * THE POSSIBILITY OF SUCH DAMAGE. >+ */ >+ >+ObjC.import("Cocoa"); >+ >+var g_isSVN; >+var g_isGit; >+var g_isGitSVN; >+var g_lastSVNInfo; >+ >+var App = Application.currentApplication(); >+App.includeStandardAdditions = true; >+ >+function run(input, parameters) { >+ var xcodeDocument = xcodeActiveDocument(); >+ if (!xcodeDocument) >+ return; >+ >+ var xcodeDocumentPath = xcodeDocument.path(); >+ determineVCSFromPath(xcodeDocumentPath); >+ >+ if (!pathIsInWebKitCheckout(xcodeDocumentPath)) >+ return; >+ >+ var lineNumber = xcodeSelectedLineInDocument(xcodeDocument); >+ var path = pathRelativeToRepositoryRootForPath(xcodeDocumentPath); >+ var revisionInfo = revisionInfoForPath(xcodeDocumentPath); >+ var annotateBlame = $.NSEvent.modifierFlags & $.NSAlternateKeyMask; >+ >+ App.setTheClipboardTo(permalinkForPath(path, lineNumber, revisionInfo, annotateBlame)); >+} >+ >+function pathIsInWebKitCheckout(path) >+{ >+ var repositoryURL = revisionInfoForPath(path).repositoryURL; >+ return !!repositoryURL.match(/^\w+:\/\/\w+\.webkit.org/); >+} >+ >+function permalinkForPath(path, lineNumber, revisionInfo, annotateBlame) >+{ >+ var revision = revisionInfo.revision ? "?rev=" + revisionInfo.revision : ""; >+ var lineNumber = lineNumber ? "#L" + lineNumber : ""; >+ var branch = revisionInfo.branch || "trunk"; >+ var withBlame = annotateBlame ? "&annotate=blame" : ""; >+ return `https://trac.webkit.org/browser/${branch}/${path}${revision}${withBlame}${lineNumber}`; >+} >+ >+// MARK: Xcode >+ >+function xcodeActiveDocument() >+{ >+ var xcode = Application("Xcode"); >+ var windows = xcode.windows(); >+ var numberOfWindows = windows.length; >+ if (!numberOfWindows) >+ return null; >+ >+ // The title of an Xcode Workspace window is the title of the document in the editor pane. >+ // Ignore windows without a name (e.g. "Edit all occurrences of a symbol" pop-up menu). >+ var documentName; >+ for (var i = 0; !documentName && i < numberOfWindows; ++i) >+ documentName = windows[i].name(); >+ if (!documentName) >+ return null; >+ >+ // The title of a modified document that has not been saved will have a suffix. Remove >+ // the suffix. >+ const editedSuffix = " â Edited"; >+ if (documentName.endsWith(editedSuffix)) >+ documentName = documentName.substr(0, documentName.lastIndexOf(editedSuffix)); >+ return xcode.documents.byName(documentName); >+} >+ >+function xcodeSelectedLineInDocument(xcodeDocument) >+{ >+ if (!xcodeDocument) >+ return -1; >+ var range = xcodeDocument.selectedCharacterRange(); >+ if (!range) >+ return -1; >+ var beginPosition = range[0] - 1; >+ if (!beginPosition) >+ return 0; >+ // FIXME: It would be more efficient to count the CRLF, CR, or LF characters >+ // in the substring from [0, beginPosition]. >+ var lines = xcodeDocument.text().split(/\r?\n|\r/); >+ var numberOfLines = lines.length; >+ var characterCount = 0; >+ var i = 0; >+ do { >+ characterCount += lines[i].length + 1; >+ if (characterCount > beginPosition) >+ break; >+ } while (++i < numberOfLines); >+ return i + 1; >+} >+ >+// MARK: VCS utilities >+ >+function determineVCSFromPath(path) >+{ >+ if (!isDirectory(path)) >+ path = dirname(path); >+ >+ g_isSVN = false; >+ g_isGit = false; >+ g_isGitSVN = false; >+ >+ if (isSVNDirectory(path)) { >+ g_isSVN = true; >+ return; >+ } >+ >+ if (isGitSVNDirectory(path)) { >+ g_isGit = true; >+ g_isGitSVN = true; >+ return; >+ } >+ >+ if (isGitDirectory(path)) { >+ g_isGit = true; >+ return; >+ } >+} >+ >+function pathRelativeToRepositoryRootForPath(path) >+{ >+ var directoryInCheckout = isDirectory(path) ? path : dirname(path); >+ if (g_isSVN) >+ return svnPathRelativeToRepositoryRootForPath(path, directoryInCheckout); >+ if (g_isGit) >+ return gitPathRelativeToRepositoryRootForPath(path, directoryInCheckout); >+ return ""; >+} >+ >+function gitPathRelativeToRepositoryRootForPath(path, directoryInCheckout) >+{ >+ return App.doShellScript(`git -C '${directoryInCheckout}' ls-tree --full-name --name-only HEAD '${path}'`); >+} >+ >+function svnPathRelativeToRepositoryRootForPath(path, directoryInCheckout) >+{ >+ return svnInfoForPath(path, directoryInCheckout).path; >+} >+ >+function revisionInfoForPath(path) >+{ >+ var directoryInCheckout = isDirectory(path) ? path : dirname(path); >+ if (g_isSVN || g_isGitSVN) >+ return svnRevisionInfoForPath(path, directoryInCheckout); >+ if (g_isGit) >+ return gitRevisionInfoForPath(path, directoryInCheckout); >+ return ""; >+} >+ >+function svnRevisionInfoForPath(path, directoryInCheckout) >+{ >+ var svnInfo = svnInfoForPath(path, directoryInCheckout); >+ return { "branch": svnInfo.branch, "revision": svnInfo.revision, "repositoryURL": svnInfo.repositoryRoot }; >+} >+ >+function gitRevisionInfoForPath(path, directoryInCheckout) >+{ >+ var repositoryURL = App.doShellScript(`git -C '${directoryInCheckout}' remote get-url origin`); >+ var revision = App.doShellScript(`git -C '${directoryInCheckout}' log -1 --format='%H' '${path}'`); >+ var branch = App.doShellScript(`git -C '${directoryInCheckout}' symbolic-ref -q HEAD`); >+ branch = branch.replace(/^refs\/heads\//, "") || "master"; >+ return { branch, revision, repositoryURL }; >+} >+ >+function svnInfoForPath(path, directoryInCheckout) >+{ >+ if (g_lastSVNInfo && g_lastSVNInfo.path === path) { >+ // FIXME: We should also ensure that the checkout directory for the cached SVN info is >+ // the same as the specified checkout directory. >+ return g_lastSVNInfo; >+ } >+ >+ var svnInfoCommand = "svn info"; >+ if (g_isGitSVN) >+ svnInfoCommand = "git " + svnInfoCommand; >+ var output = App.doShellScript(`cd '${directoryInCheckout}' && ${svnInfoCommand} '${path}'`, {"alteringLineEndings": false}); >+ if (!output) >+ return { }; >+ >+ var temp = { }; >+ var lines = output.split("\n"); >+ for (var line of lines) { >+ var [key, value] = line.split(": ", 2); >+ if (key && value) >+ temp[key] = value; >+ } >+ var svnInfo = { >+ "pathAsURL": temp["URL"], >+ "repositoryRoot": temp["Repository Root"], >+ "revision": temp["Revision"], >+ }; >+ var branch = svnInfo.pathAsURL.replace(svnInfo.repositoryRoot + "/", ""); >+ branch = branch.substr(0, branch.indexOf("/")); >+ svnInfo.branch = branch; >+ >+ // Although tempting to use temp["Path"] we cannot because it is relative to directoryInCheckout. >+ // And directoryInCheckout may not be the top-level checkout directory. We need to compute the >+ // relative path with respect to the top-level checkout directory. >+ svnInfo.path = svnInfo.pathAsURL.replace(`${svnInfo.repositoryRoot}/${branch}/`, ""); >+ >+ g_lastSVNInfo = svnInfo; >+ >+ return svnInfo; >+} >+ >+function isSVNDirectory(directory) >+{ >+ try { >+ App.doShellScript(`cd '${directory}' && svn info > /dev/null 2>&1`); >+ return true; >+ } catch (e) { >+ return false; >+ } >+} >+ >+function isGitDirectory(directory) >+{ >+ try { >+ App.doShellScript(`git -C '${directory}' rev-parse > /dev/null 2>&1`); >+ return true; >+ } catch (e) { >+ return false; >+ } >+} >+ >+function isGitSVNDirectory(directory) >+{ >+ var output = ""; >+ try { >+ output = App.doShellScript(`git -C '${directory}' config --get svn-remote.svn.fetch 2>&1`); >+ } catch (e) { } >+ return output !== ""; >+} >+ >+// MARK: Utilities >+ >+function isDirectory(path) >+{ >+ try { >+ return App.infoFor(path).folder; >+ } catch (e) { >+ return false; >+ } >+} >+ >+function dirname(path) >+{ >+ return path.substr(0, path.lastIndexOf("/")); >+} >+</string> >+ </dict> >+ <key>BundleIdentifier</key> >+ <string>com.apple.Automator.RunJavaScript</string> >+ <key>CFBundleVersion</key> >+ <string>1.0</string> >+ <key>CanShowSelectedItemsWhenRun</key> >+ <false/> >+ <key>CanShowWhenRun</key> >+ <true/> >+ <key>Category</key> >+ <array> >+ <string>AMCategoryUtilities</string> >+ </array> >+ <key>Class Name</key> >+ <string>RunJavaScriptAction</string> >+ <key>InputUUID</key> >+ <string>0C0655EF-7893-4A61-ADD0-BA803AF3C2CD</string> >+ <key>Keywords</key> >+ <array> >+ <string>Run</string> >+ <string>JavaScript</string> >+ </array> >+ <key>OutputUUID</key> >+ <string>5BAD8148-07E0-4FA2-AAA1-990A7BE926FC</string> >+ <key>UUID</key> >+ <string>24BFD6CC-7A96-42C2-8469-5D83FA921DB2</string> >+ <key>UnlocalizedApplications</key> >+ <array> >+ <string>Automator</string> >+ </array> >+ <key>arguments</key> >+ <dict> >+ <key>0</key> >+ <dict> >+ <key>default value</key> >+ <string>function run(input, parameters) { >+ >+ // Your script goes here >+ >+ return input; >+}</string> >+ <key>name</key> >+ <string>source</string> >+ <key>required</key> >+ <string>0</string> >+ <key>type</key> >+ <string>0</string> >+ <key>uuid</key> >+ <string>0</string> >+ </dict> >+ </dict> >+ <key>isViewVisible</key> >+ <true/> >+ <key>location</key> >+ <string>480.500000:316.000000</string> >+ <key>nibPath</key> >+ <string>/System/Library/Automator/Run JavaScript.action/Contents/Resources/Base.lproj/main.nib</string> >+ </dict> >+ <key>isViewVisible</key> >+ <true/> >+ </dict> >+ </array> >+ <key>connectors</key> >+ <dict/> >+ <key>workflowMetaData</key> >+ <dict> >+ <key>serviceApplicationBundleID</key> >+ <string>com.apple.dt.Xcode</string> >+ <key>serviceApplicationPath</key> >+ <string>/Applications/Xcode.app</string> >+ <key>serviceInputTypeIdentifier</key> >+ <string>com.apple.Automator.nothing</string> >+ <key>serviceOutputTypeIdentifier</key> >+ <string>com.apple.Automator.nothing</string> >+ <key>serviceProcessesInput</key> >+ <integer>0</integer> >+ <key>workflowTypeIdentifier</key> >+ <string>com.apple.Automator.servicesMenu</string> >+ </dict> >+</dict> >+</plist> >diff --git a/Tools/CopyPermalink/Xcode/INSTALL b/Tools/CopyPermalink/Xcode/INSTALL >new file mode 100644 >index 0000000000000000000000000000000000000000..449b6fb291c8a827e92aaa9d0eb7526ffb718a8a >--- /dev/null >+++ b/Tools/CopyPermalink/Xcode/INSTALL >@@ -0,0 +1,18 @@ >+Copy WebKit Permalink is an Xcode service that copies to the Clipboard a permanent hyperlink to the currently selected line in the active Xcode document. >+ >+== How to install and use == >+ >+1. Double-click "Copy WebKit Permalink.workflow". >+2. In the dialog that appears, click Install, then click Done. >+3. Choose Apple menu > System Preferences, click Keyboard, then click Shortcuts. >+4. Select Services on the left, select Copy WebKit Permalink, click in the Keyboard Shortcut field, then press the key combination that you want to use as the keyboard shortcut. >+ >+For example, press Command, Shift, Control, and C keys at the same time. >+ >+5. Enable "Copy WebKit Permalink" using the checkbox. >+ >+The Copy WebKit Permalink service will now appear under Xcode menu > Services. >+ >+== How to use == >+ >+In Xcode, use the key binding you setup above or choose Xcode > Services > Copy WebKit Permalink to copy to the Clipboard a permanent hyperlink to the selected line. Hold down the Option key when running the service to copy to the Clipboard a permanent hyperlink to the selected line with blame annotations. >\ No newline at end of file
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Flags:
joepeck
:
review+
Actions:
View
|
Formatted Diff
|
Diff
Attachments on
bug 189589
: 349679