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).

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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).