TFS Changeset History Helper

Changeset reference utilities

目前為 2016-02-17 提交的版本,檢視 最新版本

// ==UserScript==
// @name         TFS Changeset History Helper
// @namespace    http://jonas.ninja
// @version      1.1.0
// @description  Changeset reference utilities
// @author       @_jnblog
// @match        http://*/tfs/DefaultCollection/*/_versionControl/*
// @require      https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js
// @grant        GM_addStyle
// ==/UserScript==
/* jshint -W097 */
/* global $ */
/* jshint asi: true, multistr: true */
'use strict';

waitForKeyElements('.history-result', doEverything, false)
waitForKeyElements(".vc-page-title[title^=Changeset]", addChangesetIdCopyUtilities, true)

$(document).on('mouseenter', '.history-result, .ijg-changeset-id', highlightHistoryResult)
		   .on('mouseleave', '.history-result, .ijg-changeset-id', unhighlightHistoryResult)

$(document).on('click', 'input.ijg-copy-changeset-id', function clickToCopy(e) {
	// if shift is held down, copy a commit message
	var optMessage
	if (e.ctrlKey) {
		optMessage = $(this).closest('.history-result').find('.change-link').text()
		optMessage += ' (merge c' + this.value + ' to QA)'
	}
	copy(this, $(this).next('span.ijg-copy-message'), optMessage)
})
$(document).on('click', 'input.ijg-copy-changeset-page-link', function clickToCopy(e) {
	// if shift is held down, copy a commit message
	var optMessage
	if (e.ctrlKey) {
		optMessage = $('.vc-change-summary-comment').text() + ' (merge c' + this.value + ' to QA)'
	}
	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')) {
				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 changesetId = $(this).data('ijgChangesetId')
	changesetId = changesetId || $(this).find('.ijg-copy-changeset-id').data('ijgChangesetId')
	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 = '\
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;
}