- // ==UserScript==
- // @name TFS Changeset History Helper
- // @namespace http://jonas.ninja
- // @version 1.4.1
- // @description Changeset reference utilities
- // @author @_jnblog
- // @match http://*/tfs/DefaultCollection/*/_versionControl*
- // @grant GM_addStyle
- // ==/UserScript==
- /* jshint -W097 */
- /* global GM_addStyle */
- /* jshint asi: true, multistr: true */
-
- var $ = unsafeWindow.jQuery;
- var mergedChangesetRegex = /\(merge c\d{5,} to QA\)/gi
-
- waitForKeyElements('.history-result', doEverything, false)
- waitForKeyElements(".vc-page-title[title^=Changeset]", addChangesetIdCopyUtilities, true)
-
- $(document).on('mouseenter', '.history-result', highlightHistoryResult)
- .on('mouseleave', '.history-result', unhighlightHistoryResult)
-
- $(document).on('dblclick', 'input.ijg-copy-changeset-id', copyMessage)
- .on('click', 'input.ijg-copy-changeset-id', copyId)
- .on('dblclick', 'input.ijg-copy-changeset-page-link', copyPageMessage)
- .on('click', 'input.ijg-copy-changeset-page-link', copyPageId)
-
- function copyId(e) {
- if (e.ctrlKey) {
- var historyResult = $(this).closest('.history-result')
- copy(this, historyResult.data().ijgTaskId)
- } else {
- displayResult(copy(this), $(this).next('span.ijg-copy-message'), $(this).parent())
- }
- }
- function copyMessage(e) {
- var historyResult = $(this).closest('.history-result')
- displayResult(copy(this, createCommitMessage(historyResult, this.value)), $(this).next('span.ijg-copy-message'), $(this).parent(), true)
- }
-
- function copyPageId(e) {
- if (e.ctrlKey) {
- var historyResult = $(this).closest('.history-result')
- copy(this, $('.vc-change-summary-comment').text().match(/t\d{3,}/gi)[0].replace('t', ''))
- } else {
- displayResult(copy(this), $(this).next('span.ijg-copy-message'), $(this).parent())
- }
- }
- function copyPageMessage(e) {
- displayResult(copy(this, createCommitMessage(".vc-change-summary-comment", this.value)), $(this).next('span.ijg-copy-message'), $(this).parent(), true)
- }
-
- function doEverything(historyResult) {
- historyResult = $(historyResult)
- spanifyText(historyResult)
- addCopyUtilities(historyResult)
- createTaskContainers(historyResult)
-
- //fetchTaskLinks(historyResult)
- }
-
- function createTaskContainers(historyResult) {
- // makes a positioned div in the right place to hold Task info
- var tasks = historyResult.find('.ijg-task-id')
- if (tasks.size()) {
- // make a container and append rows
- var container = $('<div class="ijg-tasks-container">')
- historyResult.find('.change-link-container').append(container)
- tasks.each(function() {
- var tasknum = $(this).data('ijgTaskId')
- var task = $('<div class=ijg-task-link>').data('ijgTaskId', tasknum)
- var link = $('<a target="_blank">')
- .text(tasknum)
- .prop('href', 'http://tfs.sqlsentry.com:8080/tfs/DefaultCollection/SQLSentryWebsite/_workitems/edit/' + tasknum)
- task.append(link)
- container.append(task)
- })
- }
- }
-
- function fetchTaskLinks(historyResult) {
- var base = window.location.origin + window.location.pathname.match(/^\/(.*?)\/(.*?)\//)[0]
-
- var urls = {
- changesetLinkedWorkItems: '_apis/tfvc/changesets/{}/workItems',
- changesetInfo: '_apis/tfvc/changesets/{}',
- apiVersion: '?api-version=1.0'
- }
- }
-
- function spanifyText(historyResult) {
- // wraps changeset/task IDs with spans so they can be targeted individually
- // adds data to the newly-created spans
- historyResult.find('.change-link').each(function() {
- // commit messages may have either Tasks or Changesets
- $(this).html($(this).text().replace(/[ct]\d{3,}/gi, function(match) {
- var id = match.replace(/[ct]/gi, '')
- if (match.startsWith('t')) {
- historyResult.data('ijgTaskId', id)
- return '<span class="ijg-task-id" data-ijg-task-id="' + id + '">' + match + '</span>'
- }
- return '<span class="ijg-changeset-id" data-ijg-changeset-id="' + id + '">' + match + '</span>'
- }))
- })
- historyResult.find('.change-info').each(function() {
- // '.history-result's will only have changesets, and they will not be prefixed with 'c'
- $(this).html($(this).text().replace(/\d{3,}/gi, function(match) {
- var changesetId = match.replace(/c/i, '')
- return '<span class="ijg-changeset-id" data-ijg-changeset-id="' + changesetId + '">' + match + '</span>'
- }))
- })
- }
-
- function addCopyUtilities(historyResult) {
- // adds a text field for each changeset for easy copying of the changeset id
- var changesetId = historyResult.find('.change-info').prop('title').match(/^\d{3,6}/)[0]
- historyResult.find('.result-details')
- .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>'))
- }
-
- function addChangesetIdCopyUtilities(pageTitle) {
- var $pageTitle = $(pageTitle)
-
- if ($pageTitle.hasClass('added')) {
- return
- }
- $pageTitle.addClass('added')
-
- var id = $pageTitle.text().replace('Changeset ', '')
- var $copyLinkInput = $('<input value="' + id + '">').addClass('ijg-copy-changeset-page-link')
-
- var messageSpan = $('<span>').addClass('ijg-copy-message')
- $pageTitle.after(messageSpan).after($copyLinkInput)
- messageSpan.hide()
- }
-
- function highlightHistoryResult(e) {
- var changeset = $(this).data('changeList')
- var changesetId = changeset.changesetId
- var mainHistoryResult = $('.result-details .change-info .ijg-changeset-id[data-ijg-changeset-id=' + changesetId + ']').closest('.history-result')
- var matchingChangesets = $('span.ijg-changeset-id[data-ijg-changeset-id="' + changesetId + '"]')
-
- if (matchingChangesets.size() > 1) {
- matchingChangesets.each(function() {
- var matchingChangesetId = $(this)
- matchingChangesetId.css('color', 'red').closest('.history-result').css('background-color', 'beige')
- })
- mainHistoryResult.css('background-color', '#D1D1A9')
- }
- }
- function unhighlightHistoryResult(e) {
- $('span.ijg-changeset-id').css('color', '').closest('.history-result').css('background-color', '')
- }
-
- function displayResult(success, $messageContainer, $cursorContainer, greenCheck) {
- if (success) {
- displayBriefMessage($messageContainer, "Copied!")
- switchCursorBriefly($cursorContainer, greenCheck)
- } else {
- displayBriefMessage($messageContainer, "FAILED")
- return
- }
- }
- function displayBriefMessage($messageContainer, message) {
- $messageContainer.show().text(message)
- window.setTimeout(function() {
- $messageContainer.fadeOut(500)
- }, 1250)
- }
- function switchCursorBriefly($target, greenCheck) {
- var $targets = $target.add($target.children())
- var cursorClass = 'ijg-check' + (greenCheck ? 'Green' : '')
-
- $targets.addClass(cursorClass)
- window.setTimeout(function() {
- $targets.removeClass(cursorClass)
- }, 1750)
- }
-
- /**
- If `historyResult` is a jQuery object, expect it to contain changelist data.
- If it is a string, expect it to be a selector string that contains the full commit message.
- */
- function createCommitMessage(historyResult, changesetId) {
- var optMessage
- if (typeof historyResult === "string") {
- optMessage = $(historyResult).text().split("\n")[0]
- } else if (typeof historyResult === "object") {
- optMessage = historyResult.data().changeList.comment.split("\n")[0]
- } else {
- throw "createCommitMessage expects a string or jQuery object, but it received: " + typeof historyResult
- }
- if (optMessage.match(mergedChangesetRegex)) {
- // a changeset that's already merged to QA should merge to Release
- optMessage = optMessage.replace(mergedChangesetRegex, '(merge c' + changesetId + ' to Release)')
- } else {
- optMessage = '(merge c' + changesetId + ' to QA) ' + optMessage
- }
- return optMessage
- }
-
-
-
- var styles = '\
- img.identity-picture:first-of-type { \
- display: none; \
- } \
- img.identity-picture:only-of-type { \
- display: block; \
- } \
- span.ijg-changeset-id { \
- border-bottom: 1px dotted #ccc; \
- } \
- div > span.ijg-changeset-id { \
- cursor: default; \
- } \
- td.ijg-copy-changeset-id-container { \
- width: 52px; \
- vertical-align: top; \
- padding: 5px 7px 0 0; \
- } \
- input.ijg-copy-changeset-id { \
- cursor: pointer; \
- width: 50px; \
- text-align: center; \
- border: 1px solid #ddd; \
- padding: 3px 0; \
- margin-bottom: -2px; \
- } \
- input.ijg-copy-changeset-page-link {\
- cursor: pointer;\
- text-align: center;\
- width: 80px;\
- margin: 0 16px;\
- border: 1px solid #ccc;\
- vertical-align: middle; \
- }\
- span.ijg-copy-message { \
- display: inline-block; \
- width: 100%; \
- max-width: 60px; \
- font-size: .75em; \
- text-align: center; \
- }\
- .change-link-container { \
- position: relative;\
- display: inline-block; \
- }\
- .ijg-tasks-container {\
- position: absolute; \
- top: 0; \
- right: 0;\
- transform: translateX(100%);\
- padding-left: 20px;\
- }\
- .ijg-check {\
- cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAA7klEQVRIS7WUaw0CQQwG5xSAA5CAEyyAAywgAQfgAAeAAyQghXyXLGlgHy132+R+NTvT22470DmGznx6CDbAMxU+p2AJ3AAJzsBekrkEFp6KHyVzCHLwj2SqoAYfJVMETThwtAI1Zws8gHvj+XrgF2CXBILrBeigQi9ATcqFG56u6BueoDlJCJ4EL2BVqNZKwnCPIF3X1QxRqT3jnX8n1QNdkZq6qDRWf7mu5LNw+0w9klDluV30j6RYeWnZRSRNeGmSPRIXvLYqahI3vLWLcpIQvCVQ3kpOwKGxo37Snm2qCdanWQiHRxCG2gPdBW+zDC83C5E8rQAAAABJRU5ErkJggg==), auto !important;\
- }\
- .ijg-checkGreen {\
- 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;\
- }'
- GM_addStyle(styles)
-
-
-
-
- function copy(elToCopy, optMessage) {
- var $fakeElem = $('<textarea>');
- var succeeded
- var message = optMessage || elToCopy.value
-
- $fakeElem
- .css({
- position: 'absolute',
- left: '-9999px',
- top: (window.pageYOffset || document.documentElement.scrollTop) + 'px'
- })
- .attr('readonly', '')
- .val(message)
- .appendTo(document.body)
- select($fakeElem[0])
-
- try {
- succeeded = document.execCommand('copy')
- } catch (err) {
- succeeded = false;
- }
-
- $fakeElem.remove()
- return succeeded;
- }
-
- function select(element) {
- // MIT licensed. Author: @zenorocha
- var selectedText;
-
- if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
- element.focus();
- element.setSelectionRange(0, element.value.length);
-
- selectedText = element.value;
- } else {
- if (element.hasAttribute('contenteditable')) {
- element.focus();
- }
-
- var selection = window.getSelection();
- var range = document.createRange();
-
- range.selectNodeContents(element);
- selection.removeAllRanges();
- selection.addRange(range);
-
- selectedText = selection.toString();
- }
-
- return selectedText;
- }
-
- function waitForKeyElements(
- // CC BY-NC-SA 4.0. Author: BrockA
- selectorTxt,
- /* Required: The jQuery selector string that
- specifies the desired element(s).
- */
- actionFunction,
- /* Required: The code to run when elements are
- found. It is passed a jNode to the matched
- element.
- */
- bWaitOnce,
- /* Optional: If false, will continue to scan for
- new elements even after the first match is
- found.
- */
- iframeSelector
- /* Optional: If set, identifies the iframe to
- search.
- */
- ) {
- var targetNodes, btargetsFound;
-
- if (typeof iframeSelector == "undefined")
- targetNodes = $(selectorTxt);
- else
- targetNodes = $(iframeSelector).contents()
- .find(selectorTxt);
-
- if (targetNodes && targetNodes.length > 0) {
- btargetsFound = true;
- /*--- Found target node(s). Go through each and act if they
- are new.
- */
- targetNodes.each(function() {
- var jThis = $(this);
- var alreadyFound = jThis.data('alreadyFound') || false;
-
- if (!alreadyFound) {
- //--- Call the payload function.
- var cancelFound = actionFunction(jThis);
- if (cancelFound)
- btargetsFound = false;
- else
- jThis.data('alreadyFound', true);
- }
- });
- } else {
- btargetsFound = false;
- }
-
- //--- Get the timer-control variable for this selector.
- var controlObj = waitForKeyElements.controlObj || {};
- var controlKey = selectorTxt.replace(/[^\w]/g, "_");
- var timeControl = controlObj[controlKey];
-
- //--- Now set or clear the timer as appropriate.
- if (btargetsFound && bWaitOnce && timeControl) {
- //--- The only condition where we need to clear the timer.
- clearInterval(timeControl);
- delete controlObj[controlKey]
- } else {
- //--- Set a timer, if needed.
- if (!timeControl) {
- timeControl = setInterval(function() {
- waitForKeyElements(selectorTxt,
- actionFunction,
- bWaitOnce,
- iframeSelector
- );
- },
- 300
- );
- controlObj[controlKey] = timeControl;
- }
- }
- waitForKeyElements.controlObj = controlObj;
- }