Page MenuHomePhabricator
Paste P10653

User script to replay conflicts
ActivePublic

Authored by awight on Mar 8 2020, 10:20 PM.
Referenced Files
F31697714: raw.txt
Mar 22 2020, 10:16 AM
F31697176: raw.txt
Mar 21 2020, 9:25 PM
F31697167: raw.txt
Mar 21 2020, 9:10 PM
F31672291: raw.txt
Mar 9 2020, 6:03 AM
F31671836: raw.txt
Mar 8 2020, 10:20 PM
Subscribers
None
/**
* User script to stash and replay conflicts.
*
* The conflicting edit text is stored under a special title like,
* Conflict:<title>/<base_revision_id>/<conflict_sequence>
* Where the placeholders are, the page title where the conflict occurred, the base revision ID that was being edited,
* and a 1-based sequence which is ignored for now, and lets multiple conflicts share the same base revision.
*
* TODO:
* - Take edit summary from the saved conflict content edit summary?
* - Controls to do advanced things like tweaking the branchpoint and choosing an "other" edit other than
* the target page's latest revision.
* - Proper namespace for these pages.
* - UI labels from messages.
*/
mw.loader.using( [
'mediawiki.api',
'moment',
'oojs-ui-core'
] ).done( function () {
var api = new mw.Api();
function fetchMyRevisionContent() {
return api.get( {
action: 'query',
prop: 'revisions',
revids: mw.config.get( 'wgCurRevisionId' ),
rvprop: 'content'
} ).then(
function ( data ) {
for ( var pageId in data.query.pages ) {
for ( var index in data.query.pages[pageId].revisions ) {
return data.query.pages[pageId].revisions[index];
}
}
}
);
}
/**
* Get timestamp of the original base revision, to convince EditPage we need to conflict.
* Edit time turns out to be important, conflict cannot be triggered without it!
*/
function fetchBaseRevisionTimestamp( baseRevisionId ) {
return api.get( {
action: 'query',
prop: 'revisions',
revids: baseRevisionId,
rvprop: 'timestamp'
} ).then(
function ( data ) {
for ( var pageId in data.query.pages ) {
for ( var index in data.query.pages[pageId].revisions ) {
var revision = data.query.pages[pageId].revisions[index];
return moment( revision.timestamp ).utc().format( 'YYYYMMDDHHmmss' );
}
}
}
);
}
function buildConflictNamespaceForm( baseTitle, postFields ) {
var form = new OO.ui.FormLayout( {
// TODO: urlencode the title, or build the query using a helper.
action: mw.config.get('wgScript' ) + '?title=' + baseTitle + '&action=submit',
method: 'post'
} ),
recreateButton = new OO.ui.FieldsetLayout( {
label: 'Conflict',
items: [
new OO.ui.ButtonWidget( {
label: 'Recreate conflict',
title: 'Submit',
} ).on( 'click', function () {
// TODO: clean up binding
form.$element.submit();
} )
]
} ),
keys = Object.keys( postFields );
form.addItems( [ recreateButton ] );
for ( var i = 0; i < keys.length; i++ ) {
form.addItems( [
new OO.ui.HiddenInputWidget( {
name: keys[i],
value: postFields[keys[i]],
} )
] );
}
return form;
}
function initializeConflictNamespaceActions() {
// TODO: The regex is missing the start anchor "^" so that it can be used as a subpage in various places,
// until the Conflict namespace exists.
var conflictTitleParams = /Conflict:(.*)\/(\d+)\/(\d+)$/.exec( mw.config.get('wgPageName' ) );
if ( conflictTitleParams !== null ) {
var baseTitle = conflictTitleParams[1],
baseRevisionId = conflictTitleParams[2],
conflictSequence = conflictTitleParams[3];
$.when(
api.getEditToken(),
fetchMyRevisionContent(),
fetchBaseRevisionTimestamp( baseRevisionId )
).done(
// TODO: error handling
function ( editToken, myRevision, editTime ) {
var postFields = {
wpUnicodeCheck: 'ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ',
editRevId: baseRevisionId,
parentRevId: baseRevisionId,
// TODO: stored / input / automatic summary field.
//wpSummary: '',
wpSave: 'Save changes',
mode: 'text',
wpEditToken: editToken,
wpTextbox1: myRevision['*'],
format: myRevision.contentformat,
model: myRevision.contentmodel,
wpEdittime: editTime,
wpUltimateParam: '1',
},
form = buildConflictNamespaceForm( baseTitle, postFields );
$( '#bodyContent' ).prepend( form.$element );
}
);
}
}
function handlePostpone() {
var baseTitle = mw.config.get( 'wgPageName' ),
baseRevisionId = $( '#editform input[name="parentRevId"]' ).val(),
// TODO: Detect existing, deduplicate, and autoincrement.
conflictSequenceId = 1,
// TODO: Preference to put here or global "Conflict:"
userName = mw.config.get( 'wgUserName' ),
conflictTitle = 'User:' + userName + '/Conflict:' + baseTitle + '/' + baseRevisionId + '/' + conflictSequenceId,
conflictUrl = mw.config.get( 'wgArticlePath' ).replace( '$1', conflictTitle ),
// TODO: Use TwoColConflict merger to build potentially edited content, also compat with legacy workflow.
content = $( '#wpTextbox2' ).val() ||
$( '#editform input[name="mw-twocolconflict-your-text"]' ).val();
api.create(
conflictTitle,
{
// TODO: Take from attempted edit.
summary: 'Created by conflict userscript',
},
content
).then( function () {
window.location.href = conflictUrl;
} );
}
function buildStashButton() {
return new OO.ui.ButtonWidget( {
label: 'Postpone resolution',
} ).on( 'click', handlePostpone );
}
function initializeConflictWorkflowActions() {
if ( mw.config.get( 'wgEditMessage' ) !== 'editconflict' ) {
return;
}
$( '.cancelLink' ).prepend( buildStashButton().$element );
}
$( function () {
initializeConflictNamespaceActions();
initializeConflictWorkflowActions();
} );
});