TFS Changeset History Helper

Changeset reference utilities

目前為 2016-09-14 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name TFS Changeset History Helper
  3. // @namespace http://jonas.ninja
  4. // @version 1.4.1
  5. // @description Changeset reference utilities
  6. // @author @_jnblog
  7. // @match http://*/tfs/DefaultCollection/*/_versionControl*
  8. // @grant GM_addStyle
  9. // ==/UserScript==
  10. /* jshint -W097 */
  11. /* global GM_addStyle */
  12. /* jshint asi: true, multistr: true */
  13.  
  14. var $ = unsafeWindow.jQuery;
  15. var mergedChangesetRegex = /\(merge c\d{5,} to QA\)/gi
  16.  
  17. waitForKeyElements('.history-result', doEverything, false)
  18. waitForKeyElements(".vc-page-title[title^=Changeset]", addChangesetIdCopyUtilities, true)
  19.  
  20. $(document).on('mouseenter', '.history-result', highlightHistoryResult)
  21. .on('mouseleave', '.history-result', unhighlightHistoryResult)
  22.  
  23. $(document).on('dblclick', 'input.ijg-copy-changeset-id', copyMessage)
  24. .on('click', 'input.ijg-copy-changeset-id', copyId)
  25. .on('dblclick', 'input.ijg-copy-changeset-page-link', copyPageMessage)
  26. .on('click', 'input.ijg-copy-changeset-page-link', copyPageId)
  27.  
  28. function copyId(e) {
  29. if (e.ctrlKey) {
  30. var historyResult = $(this).closest('.history-result')
  31. copy(this, historyResult.data().ijgTaskId)
  32. } else {
  33. displayResult(copy(this), $(this).next('span.ijg-copy-message'), $(this).parent())
  34. }
  35. }
  36. function copyMessage(e) {
  37. var historyResult = $(this).closest('.history-result')
  38. displayResult(copy(this, createCommitMessage(historyResult, this.value)), $(this).next('span.ijg-copy-message'), $(this).parent(), true)
  39. }
  40.  
  41. function copyPageId(e) {
  42. if (e.ctrlKey) {
  43. var historyResult = $(this).closest('.history-result')
  44. copy(this, $('.vc-change-summary-comment').text().match(/t\d{3,}/gi)[0].replace('t', ''))
  45. } else {
  46. displayResult(copy(this), $(this).next('span.ijg-copy-message'), $(this).parent())
  47. }
  48. }
  49. function copyPageMessage(e) {
  50. displayResult(copy(this, createCommitMessage(".vc-change-summary-comment", this.value)), $(this).next('span.ijg-copy-message'), $(this).parent(), true)
  51. }
  52.  
  53. function doEverything(historyResult) {
  54. historyResult = $(historyResult)
  55. spanifyText(historyResult)
  56. addCopyUtilities(historyResult)
  57. createTaskContainers(historyResult)
  58.  
  59. //fetchTaskLinks(historyResult)
  60. }
  61.  
  62. function createTaskContainers(historyResult) {
  63. // makes a positioned div in the right place to hold Task info
  64. var tasks = historyResult.find('.ijg-task-id')
  65. if (tasks.size()) {
  66. // make a container and append rows
  67. var container = $('<div class="ijg-tasks-container">')
  68. historyResult.find('.change-link-container').append(container)
  69. tasks.each(function() {
  70. var tasknum = $(this).data('ijgTaskId')
  71. var task = $('<div class=ijg-task-link>').data('ijgTaskId', tasknum)
  72. var link = $('<a target="_blank">')
  73. .text(tasknum)
  74. .prop('href', 'http://tfs.sqlsentry.com:8080/tfs/DefaultCollection/SQLSentryWebsite/_workitems/edit/' + tasknum)
  75. task.append(link)
  76. container.append(task)
  77. })
  78. }
  79. }
  80.  
  81. function fetchTaskLinks(historyResult) {
  82. var base = window.location.origin + window.location.pathname.match(/^\/(.*?)\/(.*?)\//)[0]
  83.  
  84. var urls = {
  85. changesetLinkedWorkItems: '_apis/tfvc/changesets/{}/workItems',
  86. changesetInfo: '_apis/tfvc/changesets/{}',
  87. apiVersion: '?api-version=1.0'
  88. }
  89. }
  90.  
  91. function spanifyText(historyResult) {
  92. // wraps changeset/task IDs with spans so they can be targeted individually
  93. // adds data to the newly-created spans
  94. historyResult.find('.change-link').each(function() {
  95. // commit messages may have either Tasks or Changesets
  96. $(this).html($(this).text().replace(/[ct]\d{3,}/gi, function(match) {
  97. var id = match.replace(/[ct]/gi, '')
  98. if (match.startsWith('t')) {
  99. historyResult.data('ijgTaskId', id)
  100. return '<span class="ijg-task-id" data-ijg-task-id="' + id + '">' + match + '</span>'
  101. }
  102. return '<span class="ijg-changeset-id" data-ijg-changeset-id="' + id + '">' + match + '</span>'
  103. }))
  104. })
  105. historyResult.find('.change-info').each(function() {
  106. // '.history-result's will only have changesets, and they will not be prefixed with 'c'
  107. $(this).html($(this).text().replace(/\d{3,}/gi, function(match) {
  108. var changesetId = match.replace(/c/i, '')
  109. return '<span class="ijg-changeset-id" data-ijg-changeset-id="' + changesetId + '">' + match + '</span>'
  110. }))
  111. })
  112. }
  113.  
  114. function addCopyUtilities(historyResult) {
  115. // adds a text field for each changeset for easy copying of the changeset id
  116. var changesetId = historyResult.find('.change-info').prop('title').match(/^\d{3,6}/)[0]
  117. historyResult.find('.result-details')
  118. .before($('<td class=ijg-copy-changeset-id-container><input type=text class=ijg-copy-changeset-id data-ijg-changeset-id="' + changesetId + '" value="' + changesetId + '"><span class="ijg-copy-message"></td>'))
  119. }
  120.  
  121. function addChangesetIdCopyUtilities(pageTitle) {
  122. var $pageTitle = $(pageTitle)
  123.  
  124. if ($pageTitle.hasClass('added')) {
  125. return
  126. }
  127. $pageTitle.addClass('added')
  128.  
  129. var id = $pageTitle.text().replace('Changeset ', '')
  130. var $copyLinkInput = $('<input value="' + id + '">').addClass('ijg-copy-changeset-page-link')
  131.  
  132. var messageSpan = $('<span>').addClass('ijg-copy-message')
  133. $pageTitle.after(messageSpan).after($copyLinkInput)
  134. messageSpan.hide()
  135. }
  136.  
  137. function highlightHistoryResult(e) {
  138. var changeset = $(this).data('changeList')
  139. var changesetId = changeset.changesetId
  140. var mainHistoryResult = $('.result-details .change-info .ijg-changeset-id[data-ijg-changeset-id=' + changesetId + ']').closest('.history-result')
  141. var matchingChangesets = $('span.ijg-changeset-id[data-ijg-changeset-id="' + changesetId + '"]')
  142.  
  143. if (matchingChangesets.size() > 1) {
  144. matchingChangesets.each(function() {
  145. var matchingChangesetId = $(this)
  146. matchingChangesetId.css('color', 'red').closest('.history-result').css('background-color', 'beige')
  147. })
  148. mainHistoryResult.css('background-color', '#D1D1A9')
  149. }
  150. }
  151. function unhighlightHistoryResult(e) {
  152. $('span.ijg-changeset-id').css('color', '').closest('.history-result').css('background-color', '')
  153. }
  154.  
  155. function displayResult(success, $messageContainer, $cursorContainer, greenCheck) {
  156. if (success) {
  157. displayBriefMessage($messageContainer, "Copied!")
  158. switchCursorBriefly($cursorContainer, greenCheck)
  159. } else {
  160. displayBriefMessage($messageContainer, "FAILED")
  161. return
  162. }
  163. }
  164. function displayBriefMessage($messageContainer, message) {
  165. $messageContainer.show().text(message)
  166. window.setTimeout(function() {
  167. $messageContainer.fadeOut(500)
  168. }, 1250)
  169. }
  170. function switchCursorBriefly($target, greenCheck) {
  171. var $targets = $target.add($target.children())
  172. var cursorClass = 'ijg-check' + (greenCheck ? 'Green' : '')
  173.  
  174. $targets.addClass(cursorClass)
  175. window.setTimeout(function() {
  176. $targets.removeClass(cursorClass)
  177. }, 1750)
  178. }
  179.  
  180. /**
  181. If `historyResult` is a jQuery object, expect it to contain changelist data.
  182. If it is a string, expect it to be a selector string that contains the full commit message.
  183. */
  184. function createCommitMessage(historyResult, changesetId) {
  185. var optMessage
  186. if (typeof historyResult === "string") {
  187. optMessage = $(historyResult).text().split("\n")[0]
  188. } else if (typeof historyResult === "object") {
  189. optMessage = historyResult.data().changeList.comment.split("\n")[0]
  190. } else {
  191. throw "createCommitMessage expects a string or jQuery object, but it received: " + typeof historyResult
  192. }
  193. if (optMessage.match(mergedChangesetRegex)) {
  194. // a changeset that's already merged to QA should merge to Release
  195. optMessage = optMessage.replace(mergedChangesetRegex, '(merge c' + changesetId + ' to Release)')
  196. } else {
  197. optMessage = '(merge c' + changesetId + ' to QA) ' + optMessage
  198. }
  199. return optMessage
  200. }
  201.  
  202.  
  203.  
  204. var styles = '\
  205. img.identity-picture:first-of-type { \
  206. display: none; \
  207. } \
  208. img.identity-picture:only-of-type { \
  209. display: block; \
  210. } \
  211. span.ijg-changeset-id { \
  212. border-bottom: 1px dotted #ccc; \
  213. } \
  214. div > span.ijg-changeset-id { \
  215. cursor: default; \
  216. } \
  217. td.ijg-copy-changeset-id-container { \
  218. width: 52px; \
  219. vertical-align: top; \
  220. padding: 5px 7px 0 0; \
  221. } \
  222. input.ijg-copy-changeset-id { \
  223. cursor: pointer; \
  224. width: 50px; \
  225. text-align: center; \
  226. border: 1px solid #ddd; \
  227. padding: 3px 0; \
  228. margin-bottom: -2px; \
  229. } \
  230. input.ijg-copy-changeset-page-link {\
  231. cursor: pointer;\
  232. text-align: center;\
  233. width: 80px;\
  234. margin: 0 16px;\
  235. border: 1px solid #ccc;\
  236. vertical-align: middle; \
  237. }\
  238. span.ijg-copy-message { \
  239. display: inline-block; \
  240. width: 100%; \
  241. max-width: 60px; \
  242. font-size: .75em; \
  243. text-align: center; \
  244. }\
  245. .change-link-container { \
  246. position: relative;\
  247. display: inline-block; \
  248. }\
  249. .ijg-tasks-container {\
  250. position: absolute; \
  251. top: 0; \
  252. right: 0;\
  253. transform: translateX(100%);\
  254. padding-left: 20px;\
  255. }\
  256. .ijg-check {\
  257. cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAA7klEQVRIS7WUaw0CQQwG5xSAA5CAEyyAAywgAQfgAAeAAyQghXyXLGlgHy132+R+NTvT22470DmGznx6CDbAMxU+p2AJ3AAJzsBekrkEFp6KHyVzCHLwj2SqoAYfJVMETThwtAI1Zws8gHvj+XrgF2CXBILrBeigQi9ATcqFG56u6BueoDlJCJ4EL2BVqNZKwnCPIF3X1QxRqT3jnX8n1QNdkZq6qDRWf7mu5LNw+0w9klDluV30j6RYeWnZRSRNeGmSPRIXvLYqahI3vLWLcpIQvCVQ3kpOwKGxo37Snm2qCdanWQiHRxCG2gPdBW+zDC83C5E8rQAAAABJRU5ErkJggg==), auto !important;\
  258. }\
  259. .ijg-checkGreen {\
  260. cursor: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjI0cHgiIGhlaWdodD0iMjRweCIgdmlld0JveD0iMCAwIDQxNS41ODIgNDE1LjU4MiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNDE1LjU4MiA0MTUuNTgyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxnPgoJPHBhdGggZD0iTTQxMS40Nyw5Ni40MjZsLTQ2LjMxOS00Ni4zMmMtNS40ODItNS40ODItMTQuMzcxLTUuNDgyLTE5Ljg1MywwTDE1Mi4zNDgsMjQzLjA1OGwtODIuMDY2LTgyLjA2NCAgIGMtNS40OC01LjQ4Mi0xNC4zNy01LjQ4Mi0xOS44NTEsMGwtNDYuMzE5LDQ2LjMyYy01LjQ4Miw1LjQ4MS01LjQ4MiwxNC4zNywwLDE5Ljg1MmwxMzguMzExLDEzOC4zMSAgIGMyLjc0MSwyLjc0Miw2LjMzNCw0LjExMiw5LjkyNiw0LjExMmMzLjU5MywwLDcuMTg2LTEuMzcsOS45MjYtNC4xMTJMNDExLjQ3LDExNi4yNzdjMi42MzMtMi42MzIsNC4xMTEtNi4yMDMsNC4xMTEtOS45MjUgICBDNDE1LjU4MiwxMDIuNjI4LDQxNC4xMDMsOTkuMDU5LDQxMS40Nyw5Ni40MjZ6IiBmaWxsPSIjMmQ5ZTFlIi8+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPGc+CjwvZz4KPC9zdmc+Cg==), auto !important;\
  261. }'
  262. GM_addStyle(styles)
  263.  
  264.  
  265.  
  266.  
  267. function copy(elToCopy, optMessage) {
  268. var $fakeElem = $('<textarea>');
  269. var succeeded
  270. var message = optMessage || elToCopy.value
  271.  
  272. $fakeElem
  273. .css({
  274. position: 'absolute',
  275. left: '-9999px',
  276. top: (window.pageYOffset || document.documentElement.scrollTop) + 'px'
  277. })
  278. .attr('readonly', '')
  279. .val(message)
  280. .appendTo(document.body)
  281. select($fakeElem[0])
  282.  
  283. try {
  284. succeeded = document.execCommand('copy')
  285. } catch (err) {
  286. succeeded = false;
  287. }
  288.  
  289. $fakeElem.remove()
  290. return succeeded;
  291. }
  292.  
  293. function select(element) {
  294. // MIT licensed. Author: @zenorocha
  295. var selectedText;
  296.  
  297. if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
  298. element.focus();
  299. element.setSelectionRange(0, element.value.length);
  300.  
  301. selectedText = element.value;
  302. } else {
  303. if (element.hasAttribute('contenteditable')) {
  304. element.focus();
  305. }
  306.  
  307. var selection = window.getSelection();
  308. var range = document.createRange();
  309.  
  310. range.selectNodeContents(element);
  311. selection.removeAllRanges();
  312. selection.addRange(range);
  313.  
  314. selectedText = selection.toString();
  315. }
  316.  
  317. return selectedText;
  318. }
  319.  
  320. function waitForKeyElements(
  321. // CC BY-NC-SA 4.0. Author: BrockA
  322. selectorTxt,
  323. /* Required: The jQuery selector string that
  324. specifies the desired element(s).
  325. */
  326. actionFunction,
  327. /* Required: The code to run when elements are
  328. found. It is passed a jNode to the matched
  329. element.
  330. */
  331. bWaitOnce,
  332. /* Optional: If false, will continue to scan for
  333. new elements even after the first match is
  334. found.
  335. */
  336. iframeSelector
  337. /* Optional: If set, identifies the iframe to
  338. search.
  339. */
  340. ) {
  341. var targetNodes, btargetsFound;
  342.  
  343. if (typeof iframeSelector == "undefined")
  344. targetNodes = $(selectorTxt);
  345. else
  346. targetNodes = $(iframeSelector).contents()
  347. .find(selectorTxt);
  348.  
  349. if (targetNodes && targetNodes.length > 0) {
  350. btargetsFound = true;
  351. /*--- Found target node(s). Go through each and act if they
  352. are new.
  353. */
  354. targetNodes.each(function() {
  355. var jThis = $(this);
  356. var alreadyFound = jThis.data('alreadyFound') || false;
  357.  
  358. if (!alreadyFound) {
  359. //--- Call the payload function.
  360. var cancelFound = actionFunction(jThis);
  361. if (cancelFound)
  362. btargetsFound = false;
  363. else
  364. jThis.data('alreadyFound', true);
  365. }
  366. });
  367. } else {
  368. btargetsFound = false;
  369. }
  370.  
  371. //--- Get the timer-control variable for this selector.
  372. var controlObj = waitForKeyElements.controlObj || {};
  373. var controlKey = selectorTxt.replace(/[^\w]/g, "_");
  374. var timeControl = controlObj[controlKey];
  375.  
  376. //--- Now set or clear the timer as appropriate.
  377. if (btargetsFound && bWaitOnce && timeControl) {
  378. //--- The only condition where we need to clear the timer.
  379. clearInterval(timeControl);
  380. delete controlObj[controlKey]
  381. } else {
  382. //--- Set a timer, if needed.
  383. if (!timeControl) {
  384. timeControl = setInterval(function() {
  385. waitForKeyElements(selectorTxt,
  386. actionFunction,
  387. bWaitOnce,
  388. iframeSelector
  389. );
  390. },
  391. 300
  392. );
  393. controlObj[controlKey] = timeControl;
  394. }
  395. }
  396. waitForKeyElements.controlObj = controlObj;
  397. }