// ==UserScript==
// @name Github: unfold commit history
// @namespace http://github.com/johan/
// @description Adds "unfold all changesets" buttons (hotkey: f) above/below Commit History pages at github, letting you browse the source changes without leaving the page. (Click a commit header again to re-fold it.) You can also fold or unfold individual commits by clicking on non-link parts of the commit. As a bonus, all named commits get their tag/branch names annotated in little bubbles on the right.
// @include https://github.com/*/commits*
// @include http://github.com/*/commits*
// @match https://github.com/*/commits*
// @match http://github.com/*/commits*
// @version 1.7
// ==/UserScript==
(function exit_sandbox() { // see end of file for unsandboxing code
var toggle_options = // flip switches you configure by clicking in the UI here:
{ compact_committers: '#commit .human .actor .name span:contains("committer")'
, chain_adjacent_connected_commits: '#commit > .separator > h2'
, iso_times: '#commit .human .actor .date > abbr'
, author_filter: '.commit .human .actor:nth-child(2) .gravatar > img'
}, toggle =
{ author_filter: function(on) { $('#filtered_authors').attr('disabled',!on); }
}, options = // other options you have to edit this file for:
{ changed: true // Shows files changed, lines added / removed in folded mode
}, at = '.commit.loading .machine a[hotkey="c"]',
url = '/images/modules/browser/loading.gif',
plain = ':not(.magic):not([href*="#"])',
all = '.envelope.commit .message a:not(.loaded)'+ plain,
css = // used for .toggleClass('folded'), for, optionally, hiding:
'.file.folded > .data,\n' + // individual .commit .changeset .file:s
'.file.folded > .image,\n' + // (...or their corresponding .image:s)
'.commit.folded .changeset,\n' + // whole .commit:s' diffs,
'.commit.folded .message .full' + // + full checkin message
' { display: none; }\n' +
'.chain_adjacent_connected_commits #commit .adjacent.commit:not(.selected)' +
':not(:last-child) { border-bottom-color: transparent; }\n' +
at +':before\n { content: url("'+ url +'"); }\n'+ // show "loading" throbber
at +'\n { position: absolute; margin: 1px 0 0 -70px; height: 14px; }\n' +
'#commit .selected.loading .machine > span:nth-child(1) { border: none; }\n' +
'#commit .machine { padding-left: 14px; padding-bottom: 0; }\n' +
// The site has a .site { width: 920px } but #commit .human { width: 50em; },
// which looks bad in Opera, where it becomes about 650px only. Address this:
'#commit .human { width: 667px; }\n' +
'.fold_unfold, .download_all { float: right; }\n' +
'.all_folded .fold_unfold:before { content: "\xAB un"; }\n' +
'.all_folded .fold_unfold:after { content: " \xBB"; }\n' +
'.all_unfolded .fold_unfold:before { content: "\xBB "; }\n' +
'.all_unfolded .fold_unfold:after { content: " \xAB"; }\n' +
'#commit .human .message pre { width: auto; }\n' + // don't wrap before EOL!
'.folded .message .truncated:after { content: " (\u2026)"; }\n' +
'#commit .human .actor { width: 50%; float:left; }\n' +
'.compact_committers #commit .human .actor:nth-of-type(odd) {' +
' text-align: right; clear: none; }\n' +
'.compact_committers #commit .human .actor:nth-of-type(odd) .gravatar {' +
' float: right; margin: 0 0 0 0.7em; }\n' +
'body:not(.iso_times) .date > .iso { display: none; }\n' +
'.iso_times .date > .relatize.relatized:before { content: "("; }\n' +
'.iso_times .date > .relatize.relatized:after { content: ")"; }\n' +
'.iso_times .date > .relatize.relatized { display: inline; }\n' +
'.iso_times .date > .relatize { display: none; }\n' +
'body:not(.author_filter) #author_filter { display: none; }\n' +
'#author_filter img.filtered { opacity: 0.5; }' +
'#author_filter img { margin: 0 .3em 0 0; background-color: white; '+
' padding: 2px; border: 1px solid #D0D0D0; }' +
'.iso_times .date > .relatize.relatized:after { content: ")"; }\n' +
'.iso_times .date > .relatize.relatized { display: inline; }\n' +
'.iso_times .date > .relatize { display: none; }\n' +
'.magic.tag, .magic.branch { opacity: 0.75; }' +
'.message .tag { background: #FE7; text-align: right; padding: 0 2px; ' +
' margin: 0 -5px .1em 0; border-radius: 4px; float: right; clear: both; }\n' +
'.message .branch { background: #7EF; text-align: right; padding: 0 2px; ' +
' margin: 0 -5px .1em 0; border-radius: 4px; float: right; clear: both; }\n' +
'.magic.tag.diff { clear: left; margin-right: 0.25em; }\n' + // Δ marker
(!options.changed ? '' :
'#commit .folded .machine { padding-bottom: 0; }\n' +
'#commit .machine #toc .diffstat { border: 0; padding: 1px 0 0; }\n' +
'#commit .machine #toc .diffstat-bar { opacity: 0.75; }\n' +
'#commit .machine #toc .diffstat-summary { font-weight: normal; }\n'+
'#commit .envelope.selected .machine #toc span { border-bottom: 0; }\n' +
'#commit .machine #toc { float: right; width: 1px; margin: 0; border: 0; }');
var on_page_change =
[ prep_parent_links
, inject_commit_names
], keys = Object.keys || function _keys(o) {
var r = [], k;
for (k in o) if (o.hasOwnProperty(k)) r.push(k);
return r;
};
// Run first at init, and then once per (settled) page change, for later updates
// caused by stuff like AutoPagerize.
function onChange() {
on_page_change.forEach(function(cb, x) { cb(); });
}
function init() {
$('body').addClass('all_folded') // preload the loading throbber, so it shows
.append('<img src="'+ url +'" style="visibility:hidden;">'); // up promptly
$('head').append($('<style type="text/css"></style>').html(css));
$('.commit').live('click', toggle_commit_folding);
onChange();
$(document).bind('DOMNodeInserted', when_settled(onChange), false);
$('a[href][hotkey=p]')
.live('mouseover', null, hilight_related)
.live('mouseout', null, unlight_related)
.live('click', null, scroll_to_related); // if triggered by mouse click,
// scroll to the commit if it's in view, otherwise load that page instead --
// and ditto but for trigger by keyboard hotkey instead (falls back to link):
GitHub.Commits.link = AOP_wrap_around(try_scroll_first, GitHub.Commits.link);
init_config();
$('<div class="pagination" style="margin: 0; padding: 0;"></div>')
.prependTo('#commit .separator:first');
$('<a class="download_all" hotkey="d"><u>d</u>ecorate all</a>')
.appendTo('.pagination').click(download_all);
$('<a class="fold_unfold" hotkey="f"><u>f</u>old all</a>')
.appendTo('.pagination');
$('.fold_unfold').toggle(unfold_all, fold_all);
// export to public identifiers for the hotkeys
window.toggle_selected_folding = toggle_selected_folding;
window.toggle_all_folding = toggle_all_folding;
window.download_selected = download_selected;
window.download_all = download_all;
location.href = 'javascript:$.hotkeys(' +
'{ f: toggle_selected_folding' +
//', F: toggle_all_folding' +
', d: download_selected' +
//', D: download_all' +
'});' + // adds our own hotkeys
'delete GitHub.Commits.elements;' + // makes j / k span demand-loaded pages
'GitHub.Commits.__defineGetter__("elements",' +
'function() { return $(".commit"); });void 0';
setTimeout(function() { AOP_also_call('$.facebox.reveal', show_docs); }, 1e3);
render_author_filter();
}
// makes all authors in the view show up on top; on page load, or page update
function render_author_filter(e) {
if (e) { // postpone reruns until 100ms passed without activity
if (render_author_filter.scheduled)
clearTimeout(render_author_filter.scheduled);
render_author_filter.scheduled = setTimeout(render_author_filter, 100, 0);
return;
}
delete render_author_filter.scheduled;
if (!$('#author_filter').length) {
$('#path').after('<div id="author_filter"></div>');
$('#commit').bind('DOMSubtreeModified', render_author_filter);
}
$('.commit:not(.by) .human .actor:nth-child(2) .gravatar > img')
.each(update_author_filter); // find not-yet-catered commits
}
// makes a particular commit in the view get counted and added to author filter
function update_author_filter(e) {
var mail_hash = /avatar\/([a-f\d]{32})/.exec(this.src)
, author_id = 'author_'+ mail_hash[1]
, $gravatar = $('#'+ author_id)
, commit_no = parseInt($gravatar.attr('title') || '0', 10)
, $envelope = !commit_no && $(this).parents('.actor');
if ($envelope) {
var img = this.cloneNode(true);
img.alt = $envelope.find('.name').text().replace(/\s*\(author\)\s*$/, '');
img.id = author_id;
$('#author_filter').append(img);
$(img).click(toggle_author_commits);
img.title = ++commit_no +' commit by '+ img.alt;
$('head').append('<style id="filtered_authors"></style>');
}
else
$gravatar.attr('title',
++commit_no +' commits by '+ $gravatar.attr('alt'));
$(this).parents('.commit').addClass('by '+author_id);
$('#'+ author_id).css('padding-right', (1 + commit_no) +'px');
}
function toggle_author_commits(e) {
$(this).toggleClass('filtered');
var hide = $('#author_filter .filtered').map(function() { return this.id; });
if (hide.length) hide = '.'+ (array(hide).join(',.')) +' { display: none; }';
else hide = '';
$('#filtered_authors').html(hide);
}
// fetch some API resource by api
function github_api(path, cb) {
function get() {
if (1 === enqueue().length)
$.ajax(request);
}
function enqueue() {
var queue = github_api[path] = github_api[path] || [];
queue.push(cb); // always modify in place for dispatch
return queue;
}
function dispatch(queue, args) {
for (var i = 0, cb; cb = queue[i]; i++)
cb.apply(this, args || []);
}
var logged_in = github_api.token || $('#header a[href="/logout"]').length
, request =
{ url: path
, success: function done() {
dispatch(github_api[path], arguments);
delete github_api[path];
}
, dataType: 'json'
, beforeSend: logged_in && function(xhr) {
var name = $('#header .avatarname .name').text()
, auth = btoa(name+'/token:'+ github_api.token);
xhr.setRequestHeader('Authorization', 'Basic '+ auth);
}
};
if (!logged_in || github_api.token)
get();
else if (github_api.pending_token)
github_api.pending_token.push(get);
else {
github_api.pending_token = [get];
$.ajax({ url: '/account/admin'
, beforeSend: function(xhr) { xhr.withCredentials = true; }
, success: function(html) {
var got = html.match(/API token is <code>([^<]*)/);
if (got) {
github_api.token = got[1];
dispatch(github_api.pending_token);
delete github_api.pending_token;
}
}
});
}
}
// calls cb({ tag1: hash1, ... }, '/repo/name') after fetching the repo's tags,
// of if none, no_tags('/repo/name')
function get_tags(cb, no_tags, refresh) {
return get_named('tags', cb, no_tags, refresh);
}
// calls cb({ branch: hash1, ... }, '/repo/name') or, no_branches('/repo/name')
// (just like get_tags)
function get_branches(cb, no_branches, refresh) {
return get_named('branches', cb, no_branches, refresh);
}
function get_named(what, cb, no_cb, refresh) {
function got_names(names) {
// cache the repository's tags/branches for later
var json = window.localStorage[path] = JSON.stringify(names = names[what]);
if (json.length > 2)
cb(names, repo);
else
no_cb && no_cb(repo);
}
function get_name() { return this.textContent.replace(/ \u2713$/, ''); }
var repo = window.location.pathname.match(/^(?:\/[^\/]+){2}/);
if (repo) repo = repo[0]; else return false;
var path = what + repo
, xxxs = window.localStorage[path] && JSON.parse(window.localStorage[path])
, _css = '.subnav-bar '+ (what === 'tags' ? 'li + li' : 'li:first-child')
, page = $(_css + ' a.dropdown + ul > li').map(get_name).get().sort() || []
, have = xxxs && keys(xxxs).sort() || []
, at_b = 'branches' === what && get_current_branch();
// invalidate the branch cache if we're at the head of a branch, and its hash
// contradicts what we have saved
if (!xxxs || at_b && xxxs[at_b] !== get_first_commit_hash()) refresh = true;
// optimization - if there are no tags in the page, don't go fetch any
if ('tags' === what && !page.length) {
have = page;
xxxs = {};
refresh = false;
}
// assume the repo still has no names if it didn't at the time the page loaded
if (page.length === 0)
no_cb && no_cb(repo);
// assume the cache is still good if it's got the same tag number and names
else if (!refresh &&
have.length === page.length &&
have.join() === page.join())
cb(xxxs, repo);
else { // refresh the cache
github_api('/api/v2/json/repos/show'+ repo +'/'+ what, got_names);
return true;
}
return false;
}
function get_current_branch() {
return $('.subnav-bar li:first-child ul li strong').text().slice(0, -2);
}
function get_first_commit_hash() {
return $('#commit .commit .machine a[hotkey="c"]')[0].pathname.slice(-40);
}
// annotates commits with tag/branch names in little bubbles on the right side
function inject_commit_names() {
function draw_names(type, names, repo) {
var all_names = keys(names)
, kin_cache = {}; // kin_re => [all names matching kin_re]
all_names.sort().forEach(function(name) {
var hash = names[name]
, url = repo +'/commits/'+ name
, sel = 'a.'+ type +'[href="'+ url +'"]'
, $a = $('.commit pre > a[href$="'+ repo +'/commit/'+ hash +'"]');
if (!$a.parent().find(sel).length) { // does the commit exist in the page?
$(sel).remove(); // remove tag / branch from prior location (if any)
$a.before('<a class="magic '+type+'" href="'+ url +'">'+ name +'</a>');
// if we just linked a tag, also link a tag changeset, if applicable:
if (type !== 'tag') return;
var kin_re = quote_re(name).replace(/\d+/g, '\\d+')
, similar = new RegExp(kin_re)
, kin_tags = kin_cache[similar] = kin_cache[similar] ||
( all_names
.filter(function(tag) { return similar.test(tag); })
.sort(dwim_sort_func)
)
, this_idx = kin_tags.indexOf(name)
, last_tag = this_idx && kin_tags[this_idx - 1];
if (last_tag)
$a.before( '<a class="magic tag diff" title="Changes since '+ last_tag
+ '" href="'+ repo +'/compare/'+ last_tag +'...'+ name +'">'
+ 'Δ</a>'
);
}
});
}
function draw_tags(tags, repo) {
draw_names('tag', tags, repo);
}
function draw_branches(branches, repo) {
draw_names('branch', branches, repo);
}
var refresh = get_branches(draw_branches);
// assume it's best to refresh tags too if any branches were moved
get_tags(draw_tags, null, refresh);
}
function quote_re( re ) {
return re.replace( /([.*+^$?(){}|\x5B-\x5D])/g, "\\$1" ); // 5B-5D == [\]
}
// example usage: ['0.10', '0.9'].sort(dwim_sort_func) comes out ['0.9', '0.10']
function dwim_sort_func(a, b) {
if (a === b) return 0;
var int_str_rest_re = /^(\d*)(\D*)(.*)/
, A = int_str_rest_re.exec(a), a_int, a_str, a_int_len = A[1].length
, B = int_str_rest_re.exec(b), b_int, b_str, b_int_len = B[1].length
;
if (!a_int_len ^ !b_int_len) return a_int_len ? -1 : 1;
do {
if ((a_int = A[1]) !==
(b_int = B[1])) {
if ((a_int = parseInt(a_int, 10)) !==
(b_int = parseInt(b_int, 10)))
return a_int < b_int ? -1 : 1;
}
if ((a_str = A[2]) !==
(b_str = B[2]))
return a_str < b_str ? -1 : 1;
a = A[3];
b = B[3];
if (!a.length) return b.length ? -1 : 0;
if (!b.length) return a.length ? 1 : 0;
A = int_str_rest_re.exec(a);
B = int_str_rest_re.exec(b);
} while (true);
}
// make all commits get @id:s c_<hash>, and all parent links get @rel="<hash>"
function prep_parent_links() {
function hash(a) {
return a.pathname.slice(a.pathname.lastIndexOf('/') + 1);
}
$('.commit:not([id]) a[href][hotkey=p]').each(function reroute() {
$(this).attr('rel', hash(this));
});
$('.commit:not([id]) a[href][hotkey=c]').each(function set_id() {
var id = hash(this), ci = $(this).closest('.commit'), pr = ci.prev();
if (pr.find('a[hotkey=p][href$='+ id +']').length) pr.addClass('adjacent');
ci.attr('id', 'c_' + id);
});
$('.date > abbr.relatize:first-child').each(unrelatize_dates);
}
function unrelatize_dates() {
var ts = this.title, at = new Date(ts.replace(/-/g,'/')), t = ts.split(' ')[1]
, wd = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][at.getDay()];
$(this).before('<abbr class="iso" title="'+ ts +'">'+ wd +' '+ t +' </abbr>');
}
function try_scroll_first(wrappee, link_type) {
function normal() { return wrappee.apply(self, args); }
var args = _slice.call(arguments, 1), self = this;
if (link_type !== 'p') return normal();
var link = GitHub.Commits.selected().find('[hotkey="'+ link_type +'"]')[0];
// scroll_to_related returns true if link is not in the current view
if (link && scroll_to_related.call(link) &&
confirm('Parent commit not in view -- load parent page instead?'))
return normal();
return false;
}
function scroll_to_related(e) {
var to = $('#c_'+ this.rel);
if (!to.length) return true;
select(this.rel, true);
return false;
}
// hilight the related commit changeset, when a commit link is hovered
function hilight_related(e) {
$('#c_'+ this.rel).addClass('selected');
}
function unlight_related(e) {
$('#c_'+ this.rel).removeClass('selected');
if (null != GitHub.Commits.current)
GitHub.Commits.select(GitHub.Commits.current);
}
function show_docs(x) {
var docs =
{ f: '(un)Fold selected (or all, if none)'
, d: 'Describe selected (or all, if none)'
};
for (var key in docs)
$('#facebox .shortcuts .columns:first .column.middle dl:last')
.before('<dl class="keyboard-mappings"><dt>'+ key +'</dt>' +
'<dd>'+ docs[key] +'</dd></dl>');
return x;
}
function init_config() {
for (var o in toggle_options) {
if ((options[o] = !!window.localStorage.getItem(o)))
$('body').addClass(o);
$(toggle_options[o])
.live('click', { option: o }, toggle_option)
.live('hover', { option: o }, show_docs_for);
}
}
function toggle_option(e) {
var o = e.data.option, cb, toggle_fn = toggle[o];
if ((options[o] = !window.localStorage.getItem(o))) {
window.localStorage.setItem(o, '1');
if (toggle_fn) toggle_fn(true);
}
else {
window.localStorage.removeItem(o);
if (toggle_fn) toggle_fn(false);
}
$('body').toggleClass(o);
show_docs_for.apply(this, arguments);
return false; // do not fold / unfold
}
function show_docs_for(e) {
var o = e.data.option;
var is = !!window.localStorage.getItem(o);
$(this).css('cursor', 'pointer')
.attr('title', 'Click to toggle option "' + o.replace(/_/g, ' ') +'" '+
(is ? 'off' : 'on'));
}
function toggle_selected_folding() {
var selected = $('.selected');
if (selected.length)
selected.click();
else
toggle_all_folding();
}
function download_selected() {
var selected = $('.selected' + all);
if (selected.length)
selected.each(inline_changeset);
else
download_all();
}
function toggle_all_folding() {
if ($('body').hasClass('all_folded'))
unfold_all();
else
fold_all();
}
function download_all() {
$(all).each(inline_changeset);
}
function unfold_all() {
$('body').addClass('all_unfolded').removeClass('all_folded');
$('.commit.folded').removeClass('folded');
$(all).each(inline_and_unfold);
}
function fold_all() {
$('body').addClass('all_folded').removeClass('all_unfolded');
$('.commit').addClass('folded');
}
// click to fold / unfold, and select:
function toggle_commit_folding(e) {
if (isNotLeftButton(e) ||
$(e.target).closest('a[href], .changeset, .gravatar').length)
return; // clicked a link, or in the changeset; don't do fold action
// .magic and *# links aren't github commit links (but stuff we added)
var $link = $('.message a[href*="/commit/"]'+ plain, this);
if ($link.hasClass('loaded'))
$(this).toggleClass('folded');
else
$link.each(inline_and_unfold);
select($($(this).closest('.commit')), !'scroll');
}
// pass a changeset node, id or hash and have github select it for us
function select(changeset, scroll) {
var node = changeset, nth;
if ('string' === typeof changeset)
node = $('#'+ (/^c_/.test(changeset) ? '' : 'c_') + changeset);
nth = $('.commit').index(node);
pageCall('GitHub.Commits.select', nth);
if (scroll) setTimeout(function() {
var focused = $('.commit.selected');
//if (focused.offset().top - $(window).scrollTop() + 50 > $(window).height())
focused.scrollTo(200);
}, 50);
}
function pageCall(fn/*, arg, ... */) {
var args = JSON.stringify(_slice.call(arguments, 1)).slice(1, -1);
location.href = 'javascript:void '+ fn +'('+ args +')';
}
// every mouse click is not interesting; return true only on left mouse clicks
function isNotLeftButton(e) {
// IE has e.which === null for left click && mouseover, FF has e.which === 1
return (e.which > 1) || e.shiftKey || e.ctrlKey || e.altKey || e.metaKey;
}
function pluralize(noun, n) {
return n +' '+ noun + (n == 1 ? '' : 's');
}
function inline_and_unfold() {
var $c = $(this).closest('.commit');
inline_changeset.call(this, function() { $c.removeClass('folded'); });
}
var _slice = Array.prototype.slice;
function array(ish) {
return _slice.call(ish, 0);
}
function n(x) {
if (x > (1e9 - 5e7 - 1)) return Math.round(x / 1e9) +'G';
if (x > (1e6 - 5e4 - 1)) return Math.round(x / 1e6) +'M';
if (x > (1e3 - 5e1 - 1)) return Math.round(x / 1e3) +'k';
return x + '';
}
// loads the changeset link's full commit message, toc and the files changed and
// inlines them in the corresponding changeset (in the current page)
function inline_changeset(doneCallback) {
// make file header click toggle showing file contents (except links @ right)
function toggle_file(e) {
if (isNotLeftButton(e) || $(e.target).closest('.actions').length)
return; // wrong kind of mouse click, or a right-side action link click
$(this).parent().toggleClass('folded');
}
// diff links for this commit should refer to this commit only
function fix_link() {
var old = this.id;
this.id += '-' + sha1;
changeset.find('a[href="#'+ old +'"]')
.attr('href', '#'+ this.id);
$('div.meta', this).click(toggle_file)
.css('cursor', 'pointer')
.attr('title', 'Toggle showing of file')
.find('.actions').attr('title', ' '); // but don't over-report that title
}
function show_changed() {
var $m = $('.machine', commit), alreadyChanged = $m.find('#toc').length;
if (alreadyChanged) return;
var F = 0, A = 0, D = 0, $a = $m.append('diff' +
'<table id="toc"><tbody><tr><td class="diffstat">' +
'<a class="tooltipped leftwards"></a>' +
'</td></tr></tbody></table>').find('#toc a');
// count added / removed lines and number of files changed
$('.changeset #toc .diffstat a[title]', commit).each(function count() {
++F; // files touched
var lines = /(\d+) additions? & (\d+) deletion/.exec(this.title || '');
if (lines) {
A += Number(lines[1]); // lines added
D += Number(lines[2]); // lines deleted
}
});
var text = '<b>+'+ n(A) +'</b> / <b>-'+ n(D) +'</b> in <b>'+ n(F) +'</b>',
stat = '<span class="diffstat-summary">'+ text +'</span>\n', i, N = 5,
plus = Math.round(A / (A + D) * N), bar = '<span class="diffstat-bar">';
// don't show more blobs than total lines, and show ties as even # of blobs
if (A + D < N) { plus = A; N = A + D; } else if (A === D) { --plus; --N; }
for (i = 0; i < N; i++)
bar += '<span class="'+ (i < plus ? 'plus' : 'minus') +'">\u2022</span>';
bar += '</span>';
$a.html(stat + bar).attr('title', A +' additions & '+ D +' deletions in '+
pluralize('file', F));
}
// find all diff links and fix them, annotate how many files were changed, and
// insert line 2.. of the commit message in the unfolded view of the changeset
function post_process() {
github_inlined_comments(this);
var files = changeset.find('[id^="diff-"]').each(fix_link), line2;
if (options.changed) show_changed();
// now, add lines 2.. of the commit message to the unfolded changeset view
var whole = $('#commit', changeset); // contains the whole commit message
try {
if ((line2 = $('.message pre', whole).html().replace(line1, ''))) {
$('.human .message pre', commit).append(
$('<span class="full"></span>').html(line2)); // commit message
$('.human .message pre a.loaded' + plain, commit).after(
'<span title="Message continues..." class="truncated"></span>');
}
} catch(e) {} // if this fails, fail silent -- no biggie
whole.remove(); // and remove the remaining duplicate parts of that commit
commit.removeClass('loading'); // remove throbber
if ('function' === typeof doneCallback) doneCallback();
}
var line1 = /^[^\n]*/,
sha1 = this.pathname.slice(this.pathname.lastIndexOf('/') + 1),
commit = $(this).closest('.commit').addClass('loading folded');
$(this).addClass('loaded'); // mark that we already did load it on this page
commit.find('.human, .machine')
.css('cursor', 'pointer');
var changeset = commit
.append('<div class="changeset" style="float: left; width: 100%;"/>')
.find('.changeset') // ,#all_commit_comments removed from next line
.load(this.href + '.html #commit,#toc,#files', post_process);
}
// Makes a function that can replace wrappee that instead calls wrapper(wrappee)
// plus all the args wrappee should have received. (If wrapper does not want the
// original function to run, it does not have to.)
function AOP_wrap_around(wrapper, wrappee) {
return function() {
return wrapper.apply(this, [wrappee].concat(array(arguments)));
};
}
// replace <name> with a function that returns fn(name(...))
function AOP_also_call(name, fn) {
location.href = 'javascript:try {'+ name +' = (function(orig) {\n' +
'return function() {\n' +
'var res = orig.apply(this, arguments);\n' +
'return ('+ (fn.toString()) +')(res);' +
'};' +
'})('+ name +')} finally {void 0}';
}
// drop calls until at least <ms> (or 100) ms apart, then pass the last on to cb
function when_settled(cb, ms) {
function is_settled() {
waiter = last = null;
cb.apply(self, args);
};
ms = ms || 100;
var last, waiter, self, args;
return function () {
self = this;
args = arguments;
if (waiter) clearTimeout(waiter);
waiter = setTimeout(is_settled, 100);
};
}
// Github handlers (from http://assets1.github.com/javascripts/bundle_github.js)
// - this is all probably prone to die horribly as the site grows features, over
// time, unless this functionality gets absorbed and maintained by github later.
// In other words, everything below is really just the minimum copy-paste needed
// from the site javascript for inline comments to work -- minimal testing done.
// 5:th $(function) in http://assets1.github.com/javascripts/bundle_github.js,
// but with $() selectors scoped to a "self" node passed from the caller above.
// On unfolding changeset pages with inline comments, we need to make them live,
// as github itself is loading them dynamically after DOMContentLoaded.
function github_inlined_comments(self) {
$(".inline-comment-placeholder", self).each(function () {
var c = $(this);
$.get(c.attr("remote"), function got_comment_form(page) {
page = $(page);
c.closest("tr").replaceWith(page);
github_comment_form(page);
github_comment(page.find(".comment"));
});
});
$("#files .show-inline-comments-toggle", self).change(function () {
this.checked ? $(this).closest(".file").find("tr.inline-comments").show()
: $(this).closest(".file").find("tr.inline-comments").hide();
}).change();
$("#inline_comments_toggle input", self).change(function () {
this.checked ? $("#comments").removeClass("only-commit-comments")
: $("#comments").addClass("only-commit-comments");
}).change();
}
// http://assets1.github.com/javascripts/bundle_github.js::e(c)
function github_comment_form(c) {
c.find("ul.inline-tabs").tabs();
c.find(".show-inline-comment-form a").click(function () {
c.find(".inline-comment-form").show();
$(this).hide();
return false;
});
var b = c.find(".previewable-comment-form")
.previewableCommentForm().closest("form");
b.submit(function () {
b.find(".ajaxindicator").show();
b.find("button").attr("disabled", "disabled");
b.ajaxSubmit({
success: function (f) {
var h = b.closest(".clipper"),
d = h.find(".comment-holder");
if (d.length == 0)
d = h.prepend($('<div class="inset comment-holder"></div>'))
.find(".comment-holder");
f = $(f);
d.append(f);
github_comment(f);
b.find("textarea").val("");
b.find(".ajaxindicator").hide();
b.find("button").attr("disabled", "");
}
});
return false;
});
}
// http://assets1.github.com/javascripts/bundle_github.js::a(c)
function github_comment(c) {
c.find(".relatize").relatizeDate();
c.editableComment();
}
// This block of code injects our source in the content scope and then calls the
// passed callback there. The whole script runs in both GM and page content, but
// since we have no other code that does anything, the Greasemonkey sandbox does
// nothing at all when it has spawned the page script, which gets to use jQuery.
// (jQuery unfortunately degrades much when run in Mozilla's javascript sandbox)
if ('object' === typeof opera && opera.extension) {
this.__proto__ = window; // bleed the web page's js into our execution scope
document.addEventListener('DOMContentLoaded', init, false); // GM-style init
}
else { // for Chrome or Firefox+Greasemonkey
if ('undefined' == typeof __UNFOLD_IN_PAGE_SCOPE__) { // unsandbox, please!
var src = exit_sandbox + '',
script = document.createElement('script');
script.setAttribute('type', 'application/javascript');
script.innerHTML = 'const __UNFOLD_IN_PAGE_SCOPE__ = true;\n('+ src +')();';
document.documentElement.appendChild(script);
document.documentElement.removeChild(script);
} else { // unsandboxed -- here we go!
init();
}
}
})();