1#! /usr/bin/perl 2 3# Copyright (C) 2011 4# Jérémie Nikaes <jeremie.nikaes@ensimag.imag.fr> 5# Arnaud Lacurie <arnaud.lacurie@ensimag.imag.fr> 6# Claire Fousse <claire.fousse@ensimag.imag.fr> 7# David Amouyal <david.amouyal@ensimag.imag.fr> 8# Matthieu Moy <matthieu.moy@grenoble-inp.fr> 9# License: GPL v2 or later 10 11# Gateway between Git and MediaWiki. 12# https://github.com/Bibzball/Git-Mediawiki/wiki 13# 14# Known limitations: 15# 16# - Only wiki pages are managed, no support for [[File:...]] 17# attachments. 18# 19# - Poor performance in the best case: it takes forever to check 20# whether we're up-to-date (on fetch or push) or to fetch a few 21# revisions from a large wiki, because we use exclusively a 22# page-based synchronization. We could switch to a wiki-wide 23# synchronization when the synchronization involves few revisions 24# but the wiki is large. 25# 26# - Git renames could be turned into MediaWiki renames (see TODO 27# below) 28# 29# - login/password support requires the user to write the password 30# cleartext in a file (see TODO below). 31# 32# - No way to import "one page, and all pages included in it" 33# 34# - Multiple remote MediaWikis have not been very well tested. 35 36use strict; 37use MediaWiki::API; 38use DateTime::Format::ISO8601; 39 40# By default, use UTF-8 to communicate with Git and the user 41binmode STDERR,":utf8"; 42binmode STDOUT,":utf8"; 43 44use URI::Escape; 45use IPC::Open2; 46 47use warnings; 48 49# Mediawiki filenames can contain forward slashes. This variable decides by which pattern they should be replaced 50useconstant SLASH_REPLACEMENT =>"%2F"; 51 52# It's not always possible to delete pages (may require some 53# priviledges). Deleted pages are replaced with this content. 54useconstant DELETED_CONTENT =>"[[Category:Deleted]]\n"; 55 56# It's not possible to create empty pages. New empty files in Git are 57# sent with this content instead. 58useconstant EMPTY_CONTENT =>"<!-- empty page -->\n"; 59 60# used to reflect file creation or deletion in diff. 61useconstant NULL_SHA1 =>"0000000000000000000000000000000000000000"; 62 63my$remotename=$ARGV[0]; 64my$url=$ARGV[1]; 65 66# Accept both space-separated and multiple keys in config file. 67# Spaces should be written as _ anyway because we'll use chomp. 68my@tracked_pages=split(/[ \n]/, run_git("config --get-all remote.".$remotename.".pages")); 69chomp(@tracked_pages); 70 71# Just like @tracked_pages, but for MediaWiki categories. 72my@tracked_categories=split(/[ \n]/, run_git("config --get-all remote.".$remotename.".categories")); 73chomp(@tracked_categories); 74 75my$wiki_login= run_git("config --get remote.".$remotename.".mwLogin"); 76# TODO: ideally, this should be able to read from keyboard, but we're 77# inside a remote helper, so our stdin is connect to git, not to a 78# terminal. 79my$wiki_passwd= run_git("config --get remote.".$remotename.".mwPassword"); 80my$wiki_domain= run_git("config --get remote.".$remotename.".mwDomain"); 81chomp($wiki_login); 82chomp($wiki_passwd); 83chomp($wiki_domain); 84 85# Import only last revisions (both for clone and fetch) 86my$shallow_import= run_git("config --get --bool remote.".$remotename.".shallow"); 87chomp($shallow_import); 88$shallow_import= ($shallow_importeq"true"); 89 90# Dumb push: don't update notes and mediawiki ref to reflect the last push. 91# 92# Configurable with mediawiki.dumbPush, or per-remote with 93# remote.<remotename>.dumbPush. 94# 95# This means the user will have to re-import the just-pushed 96# revisions. On the other hand, this means that the Git revisions 97# corresponding to MediaWiki revisions are all imported from the wiki, 98# regardless of whether they were initially created in Git or from the 99# web interface, hence all users will get the same history (i.e. if 100# the push from Git to MediaWiki loses some information, everybody 101# will get the history with information lost). If the import is 102# deterministic, this means everybody gets the same sha1 for each 103# MediaWiki revision. 104my$dumb_push= run_git("config --get --bool remote.$remotename.dumbPush"); 105unless($dumb_push) { 106$dumb_push= run_git("config --get --bool mediawiki.dumbPush"); 107} 108chomp($dumb_push); 109$dumb_push= ($dumb_pusheq"true"); 110 111my$wiki_name=$url; 112$wiki_name=~s/[^\/]*:\/\///; 113# If URL is like http://user:password@example.com/, we clearly don't 114# want the password in $wiki_name. While we're there, also remove user 115# and '@' sign, to avoid author like MWUser@HTTPUser@host.com 116$wiki_name=~s/^.*@//; 117 118# Commands parser 119my$entry; 120my@cmd; 121while(<STDIN>) { 122chomp; 123@cmd=split(/ /); 124if(defined($cmd[0])) { 125# Line not blank 126if($cmd[0]eq"capabilities") { 127die("Too many arguments for capabilities")unless(!defined($cmd[1])); 128 mw_capabilities(); 129}elsif($cmd[0]eq"list") { 130die("Too many arguments for list")unless(!defined($cmd[2])); 131 mw_list($cmd[1]); 132}elsif($cmd[0]eq"import") { 133die("Invalid arguments for import")unless($cmd[1]ne""&& !defined($cmd[2])); 134 mw_import($cmd[1]); 135}elsif($cmd[0]eq"option") { 136die("Too many arguments for option")unless($cmd[1]ne""&&$cmd[2]ne""&& !defined($cmd[3])); 137 mw_option($cmd[1],$cmd[2]); 138}elsif($cmd[0]eq"push") { 139 mw_push($cmd[1]); 140}else{ 141print STDERR "Unknown command. Aborting...\n"; 142last; 143} 144}else{ 145# blank line: we should terminate 146last; 147} 148 149BEGIN{ $| =1}# flush STDOUT, to make sure the previous 150# command is fully processed. 151} 152 153########################## Functions ############################## 154 155## credential API management (generic functions) 156 157sub credential_from_url { 158my$url=shift; 159my$parsed= URI->new($url); 160my%credential; 161 162if($parsed->scheme) { 163$credential{protocol} =$parsed->scheme; 164} 165if($parsed->host) { 166$credential{host} =$parsed->host; 167} 168if($parsed->path) { 169$credential{path} =$parsed->path; 170} 171if($parsed->userinfo) { 172if($parsed->userinfo=~/([^:]*):(.*)/) { 173$credential{username} =$1; 174$credential{password} =$2; 175}else{ 176$credential{username} =$parsed->userinfo; 177} 178} 179 180return%credential; 181} 182 183sub credential_read { 184my%credential; 185my$reader=shift; 186my$op=shift; 187while(<$reader>) { 188my($key,$value) =/([^=]*)=(.*)/; 189if(not defined$key) { 190die"ERROR receiving response from git credential$op:\n$_\n"; 191} 192$credential{$key} =$value; 193} 194return%credential; 195} 196 197sub credential_write { 198my$credential=shift; 199my$writer=shift; 200while(my($key,$value) =each(%$credential) ) { 201if($value) { 202print$writer"$key=$value\n"; 203} 204} 205} 206 207sub credential_run { 208my$op=shift; 209my$credential=shift; 210my$pid= open2(my$reader,my$writer,"git credential$op"); 211 credential_write($credential,$writer); 212print$writer"\n"; 213close($writer); 214 215if($opeq"fill") { 216%$credential= credential_read($reader,$op); 217}else{ 218if(<$reader>) { 219die"ERROR while running git credential$op:\n$_"; 220} 221} 222close($reader); 223waitpid($pid,0); 224my$child_exit_status=$?>>8; 225if($child_exit_status!=0) { 226die"'git credential$op' failed with code$child_exit_status."; 227} 228} 229 230# MediaWiki API instance, created lazily. 231my$mediawiki; 232 233sub mw_connect_maybe { 234if($mediawiki) { 235return; 236} 237$mediawiki= MediaWiki::API->new; 238$mediawiki->{config}->{api_url} ="$url/api.php"; 239if($wiki_login) { 240my%credential= credential_from_url($url); 241$credential{username} =$wiki_login; 242$credential{password} =$wiki_passwd; 243 credential_run("fill", \%credential); 244my$request= {lgname =>$credential{username}, 245 lgpassword =>$credential{password}, 246 lgdomain =>$wiki_domain}; 247if($mediawiki->login($request)) { 248 credential_run("approve", \%credential); 249print STDERR "Logged in mediawiki user\"$credential{username}\".\n"; 250}else{ 251print STDERR "Failed to log in mediawiki user\"$credential{username}\"on$url\n"; 252print STDERR " (error ". 253$mediawiki->{error}->{code} .': '. 254$mediawiki->{error}->{details} .")\n"; 255 credential_run("reject", \%credential); 256exit1; 257} 258} 259} 260 261sub get_mw_first_pages { 262my$some_pages=shift; 263my@some_pages= @{$some_pages}; 264 265my$pages=shift; 266 267# pattern 'page1|page2|...' required by the API 268my$titles=join('|',@some_pages); 269 270my$mw_pages=$mediawiki->api({ 271 action =>'query', 272 titles =>$titles, 273}); 274if(!defined($mw_pages)) { 275print STDERR "fatal: could not query the list of wiki pages.\n"; 276print STDERR "fatal: '$url' does not appear to be a mediawiki\n"; 277print STDERR "fatal: make sure '$url/api.php' is a valid page.\n"; 278exit1; 279} 280while(my($id,$page) =each(%{$mw_pages->{query}->{pages}})) { 281if($id<0) { 282print STDERR "Warning: page$page->{title} not found on wiki\n"; 283}else{ 284$pages->{$page->{title}} =$page; 285} 286} 287} 288 289sub get_mw_pages { 290 mw_connect_maybe(); 291 292my%pages;# hash on page titles to avoid duplicates 293my$user_defined; 294if(@tracked_pages) { 295$user_defined=1; 296# The user provided a list of pages titles, but we 297# still need to query the API to get the page IDs. 298 299my@some_pages=@tracked_pages; 300while(@some_pages) { 301my$last=50; 302if($#some_pages<$last) { 303$last=$#some_pages; 304} 305my@slice=@some_pages[0..$last]; 306 get_mw_first_pages(\@slice, \%pages); 307@some_pages=@some_pages[51..$#some_pages]; 308} 309} 310if(@tracked_categories) { 311$user_defined=1; 312foreachmy$category(@tracked_categories) { 313if(index($category,':') <0) { 314# Mediawiki requires the Category 315# prefix, but let's not force the user 316# to specify it. 317$category="Category:".$category; 318} 319my$mw_pages=$mediawiki->list( { 320 action =>'query', 321 list =>'categorymembers', 322 cmtitle =>$category, 323 cmlimit =>'max'} ) 324||die$mediawiki->{error}->{code} .': '.$mediawiki->{error}->{details}; 325foreachmy$page(@{$mw_pages}) { 326$pages{$page->{title}} =$page; 327} 328} 329} 330if(!$user_defined) { 331# No user-provided list, get the list of pages from 332# the API. 333my$mw_pages=$mediawiki->list({ 334 action =>'query', 335 list =>'allpages', 336 aplimit =>500, 337}); 338if(!defined($mw_pages)) { 339print STDERR "fatal: could not get the list of wiki pages.\n"; 340print STDERR "fatal: '$url' does not appear to be a mediawiki\n"; 341print STDERR "fatal: make sure '$url/api.php' is a valid page.\n"; 342exit1; 343} 344foreachmy$page(@{$mw_pages}) { 345$pages{$page->{title}} =$page; 346} 347} 348returnvalues(%pages); 349} 350 351sub run_git { 352open(my$git,"-|:encoding(UTF-8)","git ".$_[0]); 353my$res=do{local$/; <$git> }; 354close($git); 355 356return$res; 357} 358 359 360sub get_last_local_revision { 361# Get note regarding last mediawiki revision 362my$note= run_git("notes --ref=$remotename/mediawikishow refs/mediawiki/$remotename/master2>/dev/null"); 363my@note_info=split(/ /,$note); 364 365my$lastrevision_number; 366if(!(defined($note_info[0]) &&$note_info[0]eq"mediawiki_revision:")) { 367print STDERR "No previous mediawiki revision found"; 368$lastrevision_number=0; 369}else{ 370# Notes are formatted : mediawiki_revision: #number 371$lastrevision_number=$note_info[1]; 372chomp($lastrevision_number); 373print STDERR "Last local mediawiki revision found is$lastrevision_number"; 374} 375return$lastrevision_number; 376} 377 378# Remember the timestamp corresponding to a revision id. 379my%basetimestamps; 380 381sub get_last_remote_revision { 382 mw_connect_maybe(); 383 384my@pages= get_mw_pages(); 385 386my$max_rev_num=0; 387 388foreachmy$page(@pages) { 389my$id=$page->{pageid}; 390 391my$query= { 392 action =>'query', 393 prop =>'revisions', 394 rvprop =>'ids|timestamp', 395 pageids =>$id, 396}; 397 398my$result=$mediawiki->api($query); 399 400my$lastrev=pop(@{$result->{query}->{pages}->{$id}->{revisions}}); 401 402$basetimestamps{$lastrev->{revid}} =$lastrev->{timestamp}; 403 404$max_rev_num= ($lastrev->{revid} >$max_rev_num?$lastrev->{revid} :$max_rev_num); 405} 406 407print STDERR "Last remote revision found is$max_rev_num.\n"; 408return$max_rev_num; 409} 410 411# Clean content before sending it to MediaWiki 412sub mediawiki_clean { 413my$string=shift; 414my$page_created=shift; 415# Mediawiki does not allow blank space at the end of a page and ends with a single \n. 416# This function right trims a string and adds a \n at the end to follow this rule 417$string=~s/\s+$//; 418if($stringeq""&&$page_created) { 419# Creating empty pages is forbidden. 420$string= EMPTY_CONTENT; 421} 422return$string."\n"; 423} 424 425# Filter applied on MediaWiki data before adding them to Git 426sub mediawiki_smudge { 427my$string=shift; 428if($stringeq EMPTY_CONTENT) { 429$string=""; 430} 431# This \n is important. This is due to mediawiki's way to handle end of files. 432return$string."\n"; 433} 434 435sub mediawiki_clean_filename { 436my$filename=shift; 437$filename=~s/@{[SLASH_REPLACEMENT]}/\//g; 438# [, ], |, {, and } are forbidden by MediaWiki, even URL-encoded. 439# Do a variant of URL-encoding, i.e. looks like URL-encoding, 440# but with _ added to prevent MediaWiki from thinking this is 441# an actual special character. 442$filename=~s/[\[\]\{\}\|]/sprintf("_%%_%x", ord($&))/ge; 443# If we use the uri escape before 444# we should unescape here, before anything 445 446return$filename; 447} 448 449sub mediawiki_smudge_filename { 450my$filename=shift; 451$filename=~s/\//@{[SLASH_REPLACEMENT]}/g; 452$filename=~s/ /_/g; 453# Decode forbidden characters encoded in mediawiki_clean_filename 454$filename=~s/_%_([0-9a-fA-F][0-9a-fA-F])/sprintf("%c", hex($1))/ge; 455return$filename; 456} 457 458sub literal_data { 459my($content) =@_; 460print STDOUT "data ", bytes::length($content),"\n",$content; 461} 462 463sub mw_capabilities { 464# Revisions are imported to the private namespace 465# refs/mediawiki/$remotename/ by the helper and fetched into 466# refs/remotes/$remotename later by fetch. 467print STDOUT "refspec refs/heads/*:refs/mediawiki/$remotename/*\n"; 468print STDOUT "import\n"; 469print STDOUT "list\n"; 470print STDOUT "push\n"; 471print STDOUT "\n"; 472} 473 474sub mw_list { 475# MediaWiki do not have branches, we consider one branch arbitrarily 476# called master, and HEAD pointing to it. 477print STDOUT "? refs/heads/master\n"; 478print STDOUT "\@refs/heads/masterHEAD\n"; 479print STDOUT "\n"; 480} 481 482sub mw_option { 483print STDERR "remote-helper command 'option$_[0]' not yet implemented\n"; 484print STDOUT "unsupported\n"; 485} 486 487sub fetch_mw_revisions_for_page { 488my$page=shift; 489my$id=shift; 490my$fetch_from=shift; 491my@page_revs= (); 492my$query= { 493 action =>'query', 494 prop =>'revisions', 495 rvprop =>'ids', 496 rvdir =>'newer', 497 rvstartid =>$fetch_from, 498 rvlimit =>500, 499 pageids =>$id, 500}; 501 502my$revnum=0; 503# Get 500 revisions at a time due to the mediawiki api limit 504while(1) { 505my$result=$mediawiki->api($query); 506 507# Parse each of those 500 revisions 508foreachmy$revision(@{$result->{query}->{pages}->{$id}->{revisions}}) { 509my$page_rev_ids; 510$page_rev_ids->{pageid} =$page->{pageid}; 511$page_rev_ids->{revid} =$revision->{revid}; 512push(@page_revs,$page_rev_ids); 513$revnum++; 514} 515last unless$result->{'query-continue'}; 516$query->{rvstartid} =$result->{'query-continue'}->{revisions}->{rvstartid}; 517} 518if($shallow_import&&@page_revs) { 519print STDERR " Found 1 revision (shallow import).\n"; 520@page_revs=sort{$b->{revid} <=>$a->{revid}} (@page_revs); 521return$page_revs[0]; 522} 523print STDERR " Found ",$revnum," revision(s).\n"; 524return@page_revs; 525} 526 527sub fetch_mw_revisions { 528my$pages=shift;my@pages= @{$pages}; 529my$fetch_from=shift; 530 531my@revisions= (); 532my$n=1; 533foreachmy$page(@pages) { 534my$id=$page->{pageid}; 535 536print STDERR "page$n/",scalar(@pages),": ".$page->{title} ."\n"; 537$n++; 538my@page_revs= fetch_mw_revisions_for_page($page,$id,$fetch_from); 539@revisions= (@page_revs,@revisions); 540} 541 542return($n,@revisions); 543} 544 545sub import_file_revision { 546my$commit=shift; 547my%commit= %{$commit}; 548my$full_import=shift; 549my$n=shift; 550 551my$title=$commit{title}; 552my$comment=$commit{comment}; 553my$content=$commit{content}; 554my$author=$commit{author}; 555my$date=$commit{date}; 556 557print STDOUT "commit refs/mediawiki/$remotename/master\n"; 558print STDOUT "mark :$n\n"; 559print STDOUT "committer$author<$author\@$wiki_name> ",$date->epoch," +0000\n"; 560 literal_data($comment); 561 562# If it's not a clone, we need to know where to start from 563if(!$full_import&&$n==1) { 564print STDOUT "from refs/mediawiki/$remotename/master^0\n"; 565} 566if($contentne DELETED_CONTENT) { 567print STDOUT "M 644 inline$title.mw\n"; 568 literal_data($content); 569print STDOUT "\n\n"; 570}else{ 571print STDOUT "D$title.mw\n"; 572} 573 574# mediawiki revision number in the git note 575if($full_import&&$n==1) { 576print STDOUT "reset refs/notes/$remotename/mediawiki\n"; 577} 578print STDOUT "commit refs/notes/$remotename/mediawiki\n"; 579print STDOUT "committer$author<$author\@$wiki_name> ",$date->epoch," +0000\n"; 580 literal_data("Note added by git-mediawiki during import"); 581if(!$full_import&&$n==1) { 582print STDOUT "from refs/notes/$remotename/mediawiki^0\n"; 583} 584print STDOUT "N inline :$n\n"; 585 literal_data("mediawiki_revision: ".$commit{mw_revision}); 586print STDOUT "\n\n"; 587} 588 589# parse a sequence of 590# <cmd> <arg1> 591# <cmd> <arg2> 592# \n 593# (like batch sequence of import and sequence of push statements) 594sub get_more_refs { 595my$cmd=shift; 596my@refs; 597while(1) { 598my$line= <STDIN>; 599if($line=~m/^$cmd (.*)$/) { 600push(@refs,$1); 601}elsif($lineeq"\n") { 602return@refs; 603}else{ 604die("Invalid command in a '$cmd' batch: ".$_); 605} 606} 607} 608 609sub mw_import { 610# multiple import commands can follow each other. 611my@refs= (shift, get_more_refs("import")); 612foreachmy$ref(@refs) { 613 mw_import_ref($ref); 614} 615print STDOUT "done\n"; 616} 617 618sub mw_import_ref { 619my$ref=shift; 620# The remote helper will call "import HEAD" and 621# "import refs/heads/master". 622# Since HEAD is a symbolic ref to master (by convention, 623# followed by the output of the command "list" that we gave), 624# we don't need to do anything in this case. 625if($refeq"HEAD") { 626return; 627} 628 629 mw_connect_maybe(); 630 631my@pages= get_mw_pages(); 632 633print STDERR "Searching revisions...\n"; 634my$last_local= get_last_local_revision(); 635my$fetch_from=$last_local+1; 636if($fetch_from==1) { 637print STDERR ", fetching from beginning.\n"; 638}else{ 639print STDERR ", fetching from here.\n"; 640} 641my($n,@revisions) = fetch_mw_revisions(\@pages,$fetch_from); 642 643# Creation of the fast-import stream 644print STDERR "Fetching & writing export data...\n"; 645 646$n=0; 647my$last_timestamp=0;# Placeholer in case $rev->timestamp is undefined 648 649foreachmy$pagerevid(sort{$a->{revid} <=>$b->{revid}}@revisions) { 650# fetch the content of the pages 651my$query= { 652 action =>'query', 653 prop =>'revisions', 654 rvprop =>'content|timestamp|comment|user|ids', 655 revids =>$pagerevid->{revid}, 656}; 657 658my$result=$mediawiki->api($query); 659 660my$rev=pop(@{$result->{query}->{pages}->{$pagerevid->{pageid}}->{revisions}}); 661 662$n++; 663 664my%commit; 665$commit{author} =$rev->{user} ||'Anonymous'; 666$commit{comment} =$rev->{comment} ||'*Empty MediaWiki Message*'; 667$commit{title} = mediawiki_smudge_filename( 668$result->{query}->{pages}->{$pagerevid->{pageid}}->{title} 669); 670$commit{mw_revision} =$pagerevid->{revid}; 671$commit{content} = mediawiki_smudge($rev->{'*'}); 672 673if(!defined($rev->{timestamp})) { 674$last_timestamp++; 675}else{ 676$last_timestamp=$rev->{timestamp}; 677} 678$commit{date} = DateTime::Format::ISO8601->parse_datetime($last_timestamp); 679 680print STDERR "$n/",scalar(@revisions),": Revision #$pagerevid->{revid} of$commit{title}\n"; 681 682 import_file_revision(\%commit, ($fetch_from==1),$n); 683} 684 685if($fetch_from==1&&$n==0) { 686print STDERR "You appear to have cloned an empty MediaWiki.\n"; 687# Something has to be done remote-helper side. If nothing is done, an error is 688# thrown saying that HEAD is refering to unknown object 0000000000000000000 689# and the clone fails. 690} 691} 692 693sub error_non_fast_forward { 694my$advice= run_git("config --bool advice.pushNonFastForward"); 695chomp($advice); 696if($advicene"false") { 697# Native git-push would show this after the summary. 698# We can't ask it to display it cleanly, so print it 699# ourselves before. 700print STDERR "To prevent you from losing history, non-fast-forward updates were rejected\n"; 701print STDERR "Merge the remote changes (e.g. 'git pull') before pushing again. See the\n"; 702print STDERR "'Note about fast-forwards' section of 'git push --help' for details.\n"; 703} 704print STDOUT "error$_[0]\"non-fast-forward\"\n"; 705return0; 706} 707 708sub mw_push_file { 709my$diff_info=shift; 710# $diff_info contains a string in this format: 711# 100644 100644 <sha1_of_blob_before_commit> <sha1_of_blob_now> <status> 712my@diff_info_split=split(/[ \t]/,$diff_info); 713 714# Filename, including .mw extension 715my$complete_file_name=shift; 716# Commit message 717my$summary=shift; 718# MediaWiki revision number. Keep the previous one by default, 719# in case there's no edit to perform. 720my$newrevid=shift; 721 722my$new_sha1=$diff_info_split[3]; 723my$old_sha1=$diff_info_split[2]; 724my$page_created= ($old_sha1eq NULL_SHA1); 725my$page_deleted= ($new_sha1eq NULL_SHA1); 726$complete_file_name= mediawiki_clean_filename($complete_file_name); 727 728if(substr($complete_file_name,-3)eq".mw") { 729my$title=substr($complete_file_name,0,-3); 730 731my$file_content; 732if($page_deleted) { 733# Deleting a page usually requires 734# special priviledges. A common 735# convention is to replace the page 736# with this content instead: 737$file_content= DELETED_CONTENT; 738}else{ 739$file_content= run_git("cat-file blob$new_sha1"); 740} 741 742 mw_connect_maybe(); 743 744my$result=$mediawiki->edit( { 745 action =>'edit', 746 summary =>$summary, 747 title =>$title, 748 basetimestamp =>$basetimestamps{$newrevid}, 749 text => mediawiki_clean($file_content,$page_created), 750}, { 751 skip_encoding =>1# Helps with names with accentuated characters 752}); 753if(!$result) { 754if($mediawiki->{error}->{code} ==3) { 755# edit conflicts, considered as non-fast-forward 756print STDERR 'Warning: Error '. 757$mediawiki->{error}->{code} . 758' from mediwiki: '.$mediawiki->{error}->{details} . 759".\n"; 760return($newrevid,"non-fast-forward"); 761}else{ 762# Other errors. Shouldn't happen => just die() 763die'Fatal: Error '. 764$mediawiki->{error}->{code} . 765' from mediwiki: '.$mediawiki->{error}->{details}; 766} 767} 768$newrevid=$result->{edit}->{newrevid}; 769print STDERR "Pushed file:$new_sha1-$title\n"; 770}else{ 771print STDERR "$complete_file_namenot a mediawiki file (Not pushable on this version of git-remote-mediawiki).\n" 772} 773return($newrevid,"ok"); 774} 775 776sub mw_push { 777# multiple push statements can follow each other 778my@refsspecs= (shift, get_more_refs("push")); 779my$pushed; 780formy$refspec(@refsspecs) { 781my($force,$local,$remote) =$refspec=~/^(\+)?([^:]*):([^:]*)$/ 782or die("Invalid refspec for push. Expected <src>:<dst> or +<src>:<dst>"); 783if($force) { 784print STDERR "Warning: forced push not allowed on a MediaWiki.\n"; 785} 786if($localeq"") { 787print STDERR "Cannot delete remote branch on a MediaWiki\n"; 788print STDOUT "error$remotecannot delete\n"; 789next; 790} 791if($remotene"refs/heads/master") { 792print STDERR "Only push to the branch 'master' is supported on a MediaWiki\n"; 793print STDOUT "error$remoteonly master allowed\n"; 794next; 795} 796if(mw_push_revision($local,$remote)) { 797$pushed=1; 798} 799} 800 801# Notify Git that the push is done 802print STDOUT "\n"; 803 804if($pushed&&$dumb_push) { 805print STDERR "Just pushed some revisions to MediaWiki.\n"; 806print STDERR "The pushed revisions now have to be re-imported, and your current branch\n"; 807print STDERR "needs to be updated with these re-imported commits. You can do this with\n"; 808print STDERR "\n"; 809print STDERR " git pull --rebase\n"; 810print STDERR "\n"; 811} 812} 813 814sub mw_push_revision { 815my$local=shift; 816my$remote=shift;# actually, this has to be "refs/heads/master" at this point. 817my$last_local_revid= get_last_local_revision(); 818print STDERR ".\n";# Finish sentence started by get_last_local_revision() 819my$last_remote_revid= get_last_remote_revision(); 820my$mw_revision=$last_remote_revid; 821 822# Get sha1 of commit pointed by local HEAD 823my$HEAD_sha1= run_git("rev-parse$local2>/dev/null");chomp($HEAD_sha1); 824# Get sha1 of commit pointed by remotes/$remotename/master 825my$remoteorigin_sha1= run_git("rev-parse refs/remotes/$remotename/master2>/dev/null"); 826chomp($remoteorigin_sha1); 827 828if($last_local_revid>0&& 829$last_local_revid<$last_remote_revid) { 830return error_non_fast_forward($remote); 831} 832 833if($HEAD_sha1eq$remoteorigin_sha1) { 834# nothing to push 835return0; 836} 837 838# Get every commit in between HEAD and refs/remotes/origin/master, 839# including HEAD and refs/remotes/origin/master 840my@commit_pairs= (); 841if($last_local_revid>0) { 842my$parsed_sha1=$remoteorigin_sha1; 843# Find a path from last MediaWiki commit to pushed commit 844while($parsed_sha1ne$HEAD_sha1) { 845my@commit_info=grep(/^$parsed_sha1/,split(/\n/, run_git("rev-list --children$local"))); 846if(!@commit_info) { 847return error_non_fast_forward($remote); 848} 849my@commit_info_split=split(/ |\n/,$commit_info[0]); 850# $commit_info_split[1] is the sha1 of the commit to export 851# $commit_info_split[0] is the sha1 of its direct child 852push(@commit_pairs, \@commit_info_split); 853$parsed_sha1=$commit_info_split[1]; 854} 855}else{ 856# No remote mediawiki revision. Export the whole 857# history (linearized with --first-parent) 858print STDERR "Warning: no common ancestor, pushing complete history\n"; 859my$history= run_git("rev-list --first-parent --children$local"); 860my@history=split('\n',$history); 861@history=@history[1..$#history]; 862foreachmy$line(reverse@history) { 863my@commit_info_split=split(/ |\n/,$line); 864push(@commit_pairs, \@commit_info_split); 865} 866} 867 868foreachmy$commit_info_split(@commit_pairs) { 869my$sha1_child= @{$commit_info_split}[0]; 870my$sha1_commit= @{$commit_info_split}[1]; 871my$diff_infos= run_git("diff-tree -r --raw -z$sha1_child$sha1_commit"); 872# TODO: we could detect rename, and encode them with a #redirect on the wiki. 873# TODO: for now, it's just a delete+add 874my@diff_info_list=split(/\0/,$diff_infos); 875# Keep the subject line of the commit message as mediawiki comment for the revision 876my$commit_msg= run_git("log --no-walk --format=\"%s\"$sha1_commit"); 877chomp($commit_msg); 878# Push every blob 879while(@diff_info_list) { 880my$status; 881# git diff-tree -z gives an output like 882# <metadata>\0<filename1>\0 883# <metadata>\0<filename2>\0 884# and we've split on \0. 885my$info=shift(@diff_info_list); 886my$file=shift(@diff_info_list); 887($mw_revision,$status) = mw_push_file($info,$file,$commit_msg,$mw_revision); 888if($statuseq"non-fast-forward") { 889# we may already have sent part of the 890# commit to MediaWiki, but it's too 891# late to cancel it. Stop the push in 892# the middle, but still give an 893# accurate error message. 894return error_non_fast_forward($remote); 895} 896if($statusne"ok") { 897die("Unknown error from mw_push_file()"); 898} 899} 900unless($dumb_push) { 901 run_git("notes --ref=$remotename/mediawikiadd -m\"mediawiki_revision:$mw_revision\"$sha1_commit"); 902 run_git("update-ref -m\"Git-MediaWiki push\"refs/mediawiki/$remotename/master$sha1_commit$sha1_child"); 903} 904} 905 906print STDOUT "ok$remote\n"; 907return1; 908}