TFS Changeset History Helper

Changeset reference utilities

目前为 2016-09-14 提交的版本。查看 最新版本

// ==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;
}