// ==UserScript==
// @name TFS Changeset History Helper
// @namespace http://jonas.ninja
// @version 1.3.0
// @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;
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('click', 'input.ijg-copy-changeset-id', clickToCopyId)
$(document).on('click', 'input.ijg-copy-changeset-page-link', clickToCopyPage)
function clickToCopyId(e) {
var historyResult = $(this).closest('.history-result')
var mergedChangesetRegex = / \(merge c\d{5,} to QA\)/gi
if (e.ctrlKey) {
// if CTRL is held down, copy first the task ID, then a commit message. Only useful if you have a clipboard manager like Ditto.
var taskId = historyResult.data('ijgTaskId')
var that = this
copy(this, $(this).next('span.ijg-copy-message'), taskId)
var optMessage = historyResult.find('.change-link').text()
if (optMessage.match(mergedChangesetRegex)) {
// a changeset that's already merged to QA should merge to Release
optMessage = optMessage.replace(mergedChangesetRegex, ' (merge c' + that.value + ' to Release)')
} else {
optMessage = optMessage.replace(/(^t\d{5,})/, '(merge c' + that.value + ' to QA)')
// optMessage += ' (merge c' + that.value + ' to QA)'
}
window.setTimeout(function() {
copy(that, $(that).next('span.ijg-copy-message'), optMessage)
}, 250)
} else {
copy(this, $(this).next('span.ijg-copy-message'))
}
}
function clickToCopyPage(e) {
// if CTRL is held down, copy a commit message
var optMessage
if (e.ctrlKey) {
optMessage = '(merge c' + this.value + ' to QA) ' + $('.vc-change-summary-comment').text()
}
copy(this, $(this).next('span.ijg-copy-message'), optMessage)
}
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')
$pageTitle.after($('<span>').addClass('copy-message')).after($copyLinkInput)
}
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', '')
}
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; \
} \
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 { \
font-size: .75em; \
display: block; \
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;\
}'
GM_addStyle(styles)
function copy(elToCopy, $messageContainer, 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;
select(elToCopy)
}
if (succeeded) {
$messageContainer.text('Copied!')
$(elToCopy).css('cursor', 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAA7klEQVRIS7WUaw0CQQwG5xSAA5CAEyyAAywgAQfgAAeAAyQghXyXLGlgHy132+R+NTvT22470DmGznx6CDbAMxU+p2AJ3AAJzsBekrkEFp6KHyVzCHLwj2SqoAYfJVMETThwtAI1Zws8gHvj+XrgF2CXBILrBeigQi9ATcqFG56u6BueoDlJCJ4EL2BVqNZKwnCPIF3X1QxRqT3jnX8n1QNdkZq6qDRWf7mu5LNw+0w9klDluV30j6RYeWnZRSRNeGmSPRIXvLYqahI3vLWLcpIQvCVQ3kpOwKGxo37Snm2qCdanWQiHRxCG2gPdBW+zDC83C5E8rQAAAABJRU5ErkJggg==), auto')
} else {
$messageContainer.text('Press Ctrl+C to copy!')
$(elToCopy).css('cursor', 'text')
}
$fakeElem.remove()
$messageContainer.show()
window.setTimeout(function() {
$messageContainer.fadeOut(500)
if (succeeded) {
$(elToCopy).css('cursor', 'pointer')
}
}, 1200)
}
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;
}