Greasy Fork

Manage Debian bug reports directly from web

Generate ready-to-send email messages to change the status of bugs (tags and user tags, severity, forwarded address).

// ==UserScript==
// @name        Manage Debian bug reports directly from web
// @description Generate ready-to-send email messages to change the status of bugs (tags and user tags, severity, forwarded address).
// @namespace   https://salsa.debian.org/gioele/
// @version     20160111.0
// @icon        https://www.debian.org/favicon.ico
// @include     https://bugs.debian.org/*
// @require     https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/jquery-details/0.1.0/jquery.details.min.js
// @grant       none
// ==/UserScript==

// prevent JQuery conflicts, see http://wiki.greasespot.net/@grant
this.$ = this.jQuery = jQuery.noConflict(true);

function bug_number_str_fn() {
	var bug_link_elem = $('h1 a').first();
	var bug_num_str = bug_link_elem.text().substr(1);

	return bug_num_str;
}

function bug_title_fn() {
	var bug_title_elem = $('h1').first();

	var bug_title_text_node = bug_title_elem
		.contents()
		.filter(function() {
			return this.nodeType == Node.TEXT_NODE;
		}).last();

	var bug_title_text = bug_title_text_node.text().trim();

	return bug_title_text;
}

function bug_package_fn() {
	var bug_package_elem = $('a.submitter').first();

	var bug_package_text = bug_package_elem.text().trim();

	return bug_package_text;
}

function bug_panel_fn() {
	var bug_panel = $('<details id="bugpanel"/>');
	bug_panel.append($("<summary>Additional fields…</summary>"));

	return bug_panel;
}

function href_for_mail(to_address, cc_addresses, reply_to_address, subject, body) {
	reply_to_address = reply_to_address || "";
	body = body || "";
	var href;

	href = "mailto:" + to_address;
	href += "?subject=" + encodeURIComponent(subject);
	for (var cc_address of cc_addresses) {
		href += "&cc=" + cc_address;
	}
	href += "&body=";
	if (reply_to_address != "") {
		href += "Reply-To: " + reply_to_address + "%0D%0A";
	}
	href += encodeURIComponent(body);

	return href;
}

function command_for_action(action, value, bug_number_str) {
	var command;

	switch (action) {
		case 'severity':
			var severity = value;
			command = 'severity ' + bug_number_str + ' ' + severity;
			break;
		case 'tags':
			var diff = value;
			command = 'tags ' + bug_number_str + ' ' + diff;
			break;
		case 'found': // FIXME: support more than one found command
			var version = value;
			command = 'found ' + bug_number_str + ' ' + version;
			break;
		case 'notfound': // FIXME: support more than one notfound command
			var version = value;
			command = 'notfound ' + bug_number_str + ' ' + version;
			break;
		case 'forwarded':
			var url = value;
			command = 'forwarded ' + bug_number_str + ' ' + url;
			break;
		case 'notforwarded':
			command = 'notforwarded ' + bug_number_str;
			break;
	}

	return command
}

function href_for_reply(bug_number_str, bug_package, bug_title, kind, actions) {
	var to_address;
	var reply_to_address = bug_number_str + "@bugs.debian.org";
	var cc_addresses = [];
	var subject = "Re: Bug#" + bug_number_str + ": " + bug_title;
	var body = "";

	// addresses

	switch (kind) {
		case 'reply':
			to_address = bug_number_str + '@bugs.debian.org';
			cc_addresses.push(bug_number_str + '[email protected]');
			break;
		case 'close':
			to_address = bug_number_str + '[email protected]';
			break;
		case 'subscribe':
			to_address = bug_number_str + '[email protected]';
			subject = subject.replace(/^Re: /, "Subscribe ");
			reply_to_address = "";
			break;
	}

	// body

	var newline = "\r\n";

	for (var action in actions) {
		var value = actions[action];
		body += 'Control: ' + command_for_action(action, value, '-1') + newline;
	}

	return href_for_mail(to_address, cc_addresses, reply_to_address, subject, body);
}

function href_for_control(bug_number_str, bug_package, bug_title, actions) {
	var to_address = "[email protected]";
	var reply_to_address;
	var cc_addresses = [];
	var subject = "";
	var body = "";

	// subject

	var action_names = Object.getOwnPropertyNames(actions);
	if (action_names.length != 0) {
		subject += action_names[0] + " ";
	}
	subject += bug_number_str + " [" + bug_package + "] " + bug_title;

	// body

	var newline = "\r\n";

	body += "package " + bug_package + newline;
	for (var action in actions) {
		var value = actions[action];
		body += command_for_action(action, value, bug_number_str) + newline;
	}
	body += "thanks" + newline;

	return href_for_mail(to_address, cc_addresses, reply_to_address, subject, body);
}

function commands_for_bts(bug_number_str, bug_package, actions) {
	var cmdline = 'bts';

	cmdline += ' package ' + bug_package;

	var first_action = true;
	for (var action in actions) {
		var value = actions[action];
		cmdline += ' , ';
		bug_id = first_action ? bug_number_str : 'it';
		cmdline += command_for_action(action, value, bug_id);
		first_action = false;
	}

	return cmdline;
}

function add_to_info_or_panel(text, field, bug_panel) {
	var field_orig = $('div.buginfo p:contains("' + text + '")');
	if (field_orig.length != 0) {
		field_orig.after(field);
		field_orig.hide();
	} else {
		bug_panel.append(field);
	}
}

function setup_bug_actions_links() {
	var reply_link_elem = $('a:contains("Reply")').first();
	reply_link_elem.attr('id', 'bug-actions-reply');

	reply_link_elem.after($('<a id="bug-actions-close" href="">close</a>'));
	reply_link_elem.after(', ');

	var subscribe_link_elem = $('a:contains("subscribe")').first();
	subscribe_link_elem.attr('id', 'bug-actions-subscribe');
}

function enhance_bug_panel() {
	var bug_number_str = bug_number_str_fn(); // FIXME
	if (bug_number_str == "") { return; }

	var bug_panel = bug_panel_fn();

	var severity_field = $('<p id="severity-field"/>');
	severity_field.append($('<span>Severity:</span>'));
	severity_field.append(' ');
	severity_field.append($('<input id="severity-orig" type="hidden"/>'));
	var severity_select = $('<select id="severity"/>');
	for (var idx in VALID_SEVERITY_LEVELS) {
		severity_level = VALID_SEVERITY_LEVELS[idx];
		severity_select.append('<option value="' + severity_level + '">' + severity_level + '</option>');
	}
	severity_field.append(severity_select);
	severity_field.on("change keyup keydown", function() { update_bug_links(); });
	add_to_info_or_panel("Severity: ", severity_field, bug_panel);

	var tags_field = $('<p id="tags-field"/>');
	tags_field.append($('<span>Tags:</span>'));
	tags_field.append(' ');
	tags_field.append($('<input id="tags-orig" name="tags-orig" type="hidden"/>'));
	tags_field.append($('<input id="tags" name="tags"/>'));
	tags_field.on("change keyup keydown", function() { update_bug_links(); });
	add_to_info_or_panel("Tags: ", tags_field, bug_panel);

	var found_field = $('<p id="found-field"/>');
	found_field.append($('<span>Found in version </span>'));
	found_field.append(' ');
	found_field.append($('<input id="found-orig" name="found-orig" type="hidden"/>'));
	found_field.append($('<input id="found" name="found"/>'));
	found_field.on("change keyup keydown", function() { update_bug_links(); });
	add_to_info_or_panel("Found in version", found_field, bug_panel);

	var forwarded_field = $('<p id="forwarded-url-field"/>');
	forwarded_field.append($('<span>Forwarded to</span>'));
	forwarded_field.append(' ');
	forwarded_field.append($('<input id="forwarded-url-orig" name="forwarded-url-orig" type="hidden"/>'));
	forwarded_field.append($('<input id="forwarded-url" name="forwarded-url" size="60"/>'));
	forwarded_field.on("change keyup keydown", function() { update_bug_links(); });
	add_to_info_or_panel("Forwarded to", forwarded_field, bug_panel);

	var bug_commands_elem = $('<p id="bug-commands"/>');
	bug_commands_elem.append($('<span>Scheduled bug commands</span>'));
	bug_commands_elem.append(' ');
	bug_commands_elem.append($('<span id="bug-commands-count"/>'));
	bug_commands_elem.append(' ');
	bug_commands_elem.append($('<a id="bug-commands-submit-via-control" href="mailto:[email protected]">submit via control</a>'));
	bug_commands_elem.append(', ');
	var submit_via_bts_elem = $('<a id="bug-commands-submit-via-bts" href="">via bts</a>');
	submit_via_bts_elem.on('click', function() { $('p#bts-commands-field').show(100); return false; });
	bug_commands_elem.append(submit_via_bts_elem);

	var bts_commands_field = $('<p id="bts-commands-field" style="padding-left: 1em"/>');
	bts_commands_field.append('Use this command line: ');
	bts_commands_field.append(' ');
	bts_commands_field.append($('<input id="bts-commands" readonly="readonly"/>'));
	bts_commands_field.hide();

	var bug_command_list_elem = $('<ul id="bug-command-list"/>');

	var buginfo_elem = $('div.buginfo').last();
	buginfo_elem.after(bug_command_list_elem);
	buginfo_elem.after(bts_commands_field);
	buginfo_elem.after(bug_commands_elem);
	buginfo_elem.after(bug_panel);

	set_initial_values();
	setup_bug_actions_links();
	update_bug_links();
}

function set_initial_values() {
	// Severity
	var severity_orig_elem = $('div.buginfo p:contains("Severity: ")').first();
	var severity_orig = severity_orig_elem.text().replace(/^Severity: /, "");
	$('input#severity-orig').val(severity_orig);
	$('select#severity').val(severity_orig);

	// Tags
	var tags_orig_elem = $('div.buginfo p:contains("Tags: ")').first();
	var tags_orig = tags_orig_elem.text().replace(/^Tags: /, "");
	$('input#tags-orig').val(tags_orig);
	$('input#tags').val(tags_orig);

	// Found
	var found_orig_elem = $('div.buginfo p:contains("Found in version")').first();
	var found_orig = found_orig_elem.text().replace(/^Found in versions? /, "");
	$('input#found-orig').val(found_orig);
	$('input#found').val(found_orig);

	// Forwarded to
	var forwarded_url_orig_elem = $('div.buginfo p:contains("Forwarded to") a').first();
	var forwarded_url_orig = forwarded_url_orig_elem.attr("href") || "";
	$('input#forwarded-url-orig').val(forwarded_url_orig);
	$('input#forwarded-url').val(forwarded_url_orig);
}

function update_bug_links() {
	var bug_number_str = bug_number_str_fn(); // FIXME
	var bug_title = bug_title_fn();
	var bug_package = bug_package_fn();

	////////////////////////////////////////

	var actions = {}

	var bug_command_list_elem = $('#bug-command-list');
	bug_command_list_elem.empty();

	// severity
	var severity_orig = $('input#severity-orig').val();
	var severity = $('select#severity').val();
	if (severity != severity_orig) {
		bug_command_list_elem.append($('<li>severity ' + severity + ' (was <em>' + severity_orig + '</em>)</li>'));
		actions['severity'] = severity;
	}

	// tags
	var tags_orig = $('input#tags-orig').val();
	var tags = $('input#tags').val();

	function split_tags(value) {
		var pieces = value.split(",");
		pieces = pieces.map(t => t.trim());
		pieces = pieces.filter(t => t != "");
		pieces = pieces.filter(function(el,i,a) { return (i==a.indexOf(el)); });
		return pieces;
	}
	tags_orig = split_tags(tags_orig);
	tags = split_tags(tags);

	var tags_added = tags.filter(tag => tags_orig.indexOf(tag) < 0);
	var tags_removed = tags_orig.filter(tag => tags.indexOf(tag) < 0);
	if (tags_added.length + tags_removed.length != 0) {
		for (var tag_idx in tags_added) {
			bug_command_list_elem.append($('<li>tags + <add>' + tags_added[tag_idx] + '</add></li>'));
		}
		for (var tag_idx in tags_removed) {
			bug_command_list_elem.append($('<li>tags - <del>' + tags_removed[tag_idx] + '</del></li>'));
		}

		var tag_actions = [];
		if (tags_added.length > 0) {
			tag_actions.push(['+ ' + tags_added.join(' ')]);
		}
		if (tags_removed.length > 0) {
			tag_actions.push(['- ' + tags_removed.join(' ')]);
		}
		actions['tags'] = tag_actions.join(' ');
	}

	// found
	var found_orig = $('input#found-orig').val();
	var found = $('input#found').val();

	found_orig = split_tags(found_orig);
	found = split_tags(found);

	var found_added = found.filter(ver => found_orig.indexOf(ver) < 0);
	var found_removed = found_orig.filter(ver => found.indexOf(ver) < 0);
	if (found_added.length + found_removed.length != 0) {
		for (var ver_idx in found_added) {
			bug_command_list_elem.append($('<li>found <add>' + found_added[ver_idx] + '</add></li>'));
		}
		for (var ver_idx in found_removed) {
			bug_command_list_elem.append($('<li>notfound <del>' + found_removed[ver_idx] + '</del></li>'));
		}
	}
	if (found_added.length > 0) {
		actions['found'] = found_added[0]; // FIXME: add more than one version
	}
	if (found_removed.length > 0) {
		actions['notfound'] = found_removed[0]; // FIXME: remove more than one version
	}

	// forwarded
	var forwarded_url_orig = $('input#forwarded-url-orig').val();
	var forwarded_url = $('input#forwarded-url').val();
	if (forwarded_url != forwarded_url_orig) {
		var str;
		if (forwarded_url != "") {
			str = 'forwarded <a href="' + forwarded_url + '">' + forwarded_url + '</a>';
		} else {
			str = 'notforwarded';
		}

		var was;
		if (forwarded_url_orig == "") {
			was = " (was <em>unset</em>)";
		} else {
			was = ' (was <a href="' + forwarded_url_orig + '">' + forwarded_url_orig + '</a>)';
		}

		bug_command_list_elem.append($('<li>' + str + ' ' + was + '</li>'));

		if (forwarded_url != "") {
			actions['forwarded'] = forwarded_url;
		} else {
			actions['notforwarded'] = "";
		}
	}

	// update command list and links

	var commands_count = bug_command_list_elem.children().length;
	if (commands_count == 0) {
		commands_count = "none";
	}
	$('#bug-commands-count').text("(" + commands_count + ")");

	var reply_mail_href = href_for_reply(bug_number_str, bug_package, bug_title, 'reply', actions);
	var close_mail_href = href_for_reply(bug_number_str, bug_package, bug_title, 'close', actions);
	var subscribe_mail_href = href_for_reply(bug_number_str, bug_package, bug_title, 'subscribe', actions);
	$('a#bug-actions-reply').attr("href", reply_mail_href);
	$('a#bug-actions-close').attr("href", close_mail_href);
	$('a#bug-actions-subscribe').attr("href", subscribe_mail_href);

	var control_mail_href = href_for_control(bug_number_str, bug_package, bug_title, actions);
	$('a#bug-commands-submit-via-control').attr("href", control_mail_href);

	var bts_commands = commands_for_bts(bug_number_str, bug_package, actions);
	$('input#bts-commands').val(bts_commands);
}

function add_style_elem() {
	var style_str = "\
#bugpanel, #bugpanel p {\
	font-family: sans-serif; font-size: 110%;\
	margin: 0; border: 0;\
}\
#bugpanel input, .buginfo input,\
#bugpanel select, .buginfo select,\
#bts-commands-field input {\
	margin-top: 0; margin-bottom: 0;\
	border: thin solid #cecece;\
	background-color: inherit;\
}\
#bugpanel > * { padding-left: 2em; }\
#bugpanel summary { padding-left: inherit; }\
p#bug-commands { margin: 0; font-size: 90%; color: #686868; }\
";
	$('body').append($('<style type="text/css">' + style_str + '</style>'));
}

var VALID_SEVERITY_LEVELS = [ 'critical', 'grave', 'serious', 'important', 'normal', 'minor', 'wishlist' ];
var VALID_TAGS = [ 'patch', 'wontfix', 'moreinfo', 'unreproducible', 'help', 'pending', 'security', 'upstream', 'confirmed', 'fixed', 'fixed-upstream', 'fixed-in-experimental', 'd-i', 'ipv6', 'lfs', 'l10n', 'potato', 'woody', 'sarge', 'sarge-ignore', 'etch', 'etch-ignore', 'lenny', 'lenny-ignore', 'squeeze', 'squeeze-ignore', 'wheezy', 'wheezy-ignore', 'jessie', 'jessie-ignore', 'sid', 'experimental' ]

add_style_elem();
enhance_bug_panel();
$('details').details();

// This is free software released into the public domain (CC0 license).