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