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 351# usage: $out = run_git("command args"); 352# $out = run_git("command args", "raw"); # don't interpret output as UTF-8. 353sub run_git { 354my$args=shift; 355my$encoding= (shift||"encoding(UTF-8)"); 356open(my$git,"-|:$encoding","git ".$args); 357my$res=do{local$/; <$git> }; 358close($git); 359 360return$res; 361} 362 363 364sub get_last_local_revision { 365# Get note regarding last mediawiki revision 366my$note= run_git("notes --ref=$remotename/mediawikishow refs/mediawiki/$remotename/master2>/dev/null"); 367my@note_info=split(/ /,$note); 368 369my$lastrevision_number; 370if(!(defined($note_info[0]) &&$note_info[0]eq"mediawiki_revision:")) { 371print STDERR "No previous mediawiki revision found"; 372$lastrevision_number=0; 373}else{ 374# Notes are formatted : mediawiki_revision: #number 375$lastrevision_number=$note_info[1]; 376chomp($lastrevision_number); 377print STDERR "Last local mediawiki revision found is$lastrevision_number"; 378} 379return$lastrevision_number; 380} 381 382# Remember the timestamp corresponding to a revision id. 383my%basetimestamps; 384 385sub get_last_remote_revision { 386 mw_connect_maybe(); 387 388my@pages= get_mw_pages(); 389 390my$max_rev_num=0; 391 392foreachmy$page(@pages) { 393my$id=$page->{pageid}; 394 395my$query= { 396 action =>'query', 397 prop =>'revisions', 398 rvprop =>'ids|timestamp', 399 pageids =>$id, 400}; 401 402my$result=$mediawiki->api($query); 403 404my$lastrev=pop(@{$result->{query}->{pages}->{$id}->{revisions}}); 405 406$basetimestamps{$lastrev->{revid}} =$lastrev->{timestamp}; 407 408$max_rev_num= ($lastrev->{revid} >$max_rev_num?$lastrev->{revid} :$max_rev_num); 409} 410 411print STDERR "Last remote revision found is$max_rev_num.\n"; 412return$max_rev_num; 413} 414 415# Clean content before sending it to MediaWiki 416sub mediawiki_clean { 417my$string=shift; 418my$page_created=shift; 419# Mediawiki does not allow blank space at the end of a page and ends with a single \n. 420# This function right trims a string and adds a \n at the end to follow this rule 421$string=~s/\s+$//; 422if($stringeq""&&$page_created) { 423# Creating empty pages is forbidden. 424$string= EMPTY_CONTENT; 425} 426return$string."\n"; 427} 428 429# Filter applied on MediaWiki data before adding them to Git 430sub mediawiki_smudge { 431my$string=shift; 432if($stringeq EMPTY_CONTENT) { 433$string=""; 434} 435# This \n is important. This is due to mediawiki's way to handle end of files. 436return$string."\n"; 437} 438 439sub mediawiki_clean_filename { 440my$filename=shift; 441$filename=~s/@{[SLASH_REPLACEMENT]}/\//g; 442# [, ], |, {, and } are forbidden by MediaWiki, even URL-encoded. 443# Do a variant of URL-encoding, i.e. looks like URL-encoding, 444# but with _ added to prevent MediaWiki from thinking this is 445# an actual special character. 446$filename=~s/[\[\]\{\}\|]/sprintf("_%%_%x", ord($&))/ge; 447# If we use the uri escape before 448# we should unescape here, before anything 449 450return$filename; 451} 452 453sub mediawiki_smudge_filename { 454my$filename=shift; 455$filename=~s/\//@{[SLASH_REPLACEMENT]}/g; 456$filename=~s/ /_/g; 457# Decode forbidden characters encoded in mediawiki_clean_filename 458$filename=~s/_%_([0-9a-fA-F][0-9a-fA-F])/sprintf("%c", hex($1))/ge; 459return$filename; 460} 461 462sub literal_data { 463my($content) =@_; 464print STDOUT "data ", bytes::length($content),"\n",$content; 465} 466 467sub mw_capabilities { 468# Revisions are imported to the private namespace 469# refs/mediawiki/$remotename/ by the helper and fetched into 470# refs/remotes/$remotename later by fetch. 471print STDOUT "refspec refs/heads/*:refs/mediawiki/$remotename/*\n"; 472print STDOUT "import\n"; 473print STDOUT "list\n"; 474print STDOUT "push\n"; 475print STDOUT "\n"; 476} 477 478sub mw_list { 479# MediaWiki do not have branches, we consider one branch arbitrarily 480# called master, and HEAD pointing to it. 481print STDOUT "? refs/heads/master\n"; 482print STDOUT "\@refs/heads/masterHEAD\n"; 483print STDOUT "\n"; 484} 485 486sub mw_option { 487print STDERR "remote-helper command 'option$_[0]' not yet implemented\n"; 488print STDOUT "unsupported\n"; 489} 490 491sub fetch_mw_revisions_for_page { 492my$page=shift; 493my$id=shift; 494my$fetch_from=shift; 495my@page_revs= (); 496my$query= { 497 action =>'query', 498 prop =>'revisions', 499 rvprop =>'ids', 500 rvdir =>'newer', 501 rvstartid =>$fetch_from, 502 rvlimit =>500, 503 pageids =>$id, 504}; 505 506my$revnum=0; 507# Get 500 revisions at a time due to the mediawiki api limit 508while(1) { 509my$result=$mediawiki->api($query); 510 511# Parse each of those 500 revisions 512foreachmy$revision(@{$result->{query}->{pages}->{$id}->{revisions}}) { 513my$page_rev_ids; 514$page_rev_ids->{pageid} =$page->{pageid}; 515$page_rev_ids->{revid} =$revision->{revid}; 516push(@page_revs,$page_rev_ids); 517$revnum++; 518} 519last unless$result->{'query-continue'}; 520$query->{rvstartid} =$result->{'query-continue'}->{revisions}->{rvstartid}; 521} 522if($shallow_import&&@page_revs) { 523print STDERR " Found 1 revision (shallow import).\n"; 524@page_revs=sort{$b->{revid} <=>$a->{revid}} (@page_revs); 525return$page_revs[0]; 526} 527print STDERR " Found ",$revnum," revision(s).\n"; 528return@page_revs; 529} 530 531sub fetch_mw_revisions { 532my$pages=shift;my@pages= @{$pages}; 533my$fetch_from=shift; 534 535my@revisions= (); 536my$n=1; 537foreachmy$page(@pages) { 538my$id=$page->{pageid}; 539 540print STDERR "page$n/",scalar(@pages),": ".$page->{title} ."\n"; 541$n++; 542my@page_revs= fetch_mw_revisions_for_page($page,$id,$fetch_from); 543@revisions= (@page_revs,@revisions); 544} 545 546return($n,@revisions); 547} 548 549sub import_file_revision { 550my$commit=shift; 551my%commit= %{$commit}; 552my$full_import=shift; 553my$n=shift; 554 555my$title=$commit{title}; 556my$comment=$commit{comment}; 557my$content=$commit{content}; 558my$author=$commit{author}; 559my$date=$commit{date}; 560 561print STDOUT "commit refs/mediawiki/$remotename/master\n"; 562print STDOUT "mark :$n\n"; 563print STDOUT "committer$author<$author\@$wiki_name> ",$date->epoch," +0000\n"; 564 literal_data($comment); 565 566# If it's not a clone, we need to know where to start from 567if(!$full_import&&$n==1) { 568print STDOUT "from refs/mediawiki/$remotename/master^0\n"; 569} 570if($contentne DELETED_CONTENT) { 571print STDOUT "M 644 inline$title.mw\n"; 572 literal_data($content); 573print STDOUT "\n\n"; 574}else{ 575print STDOUT "D$title.mw\n"; 576} 577 578# mediawiki revision number in the git note 579if($full_import&&$n==1) { 580print STDOUT "reset refs/notes/$remotename/mediawiki\n"; 581} 582print STDOUT "commit refs/notes/$remotename/mediawiki\n"; 583print STDOUT "committer$author<$author\@$wiki_name> ",$date->epoch," +0000\n"; 584 literal_data("Note added by git-mediawiki during import"); 585if(!$full_import&&$n==1) { 586print STDOUT "from refs/notes/$remotename/mediawiki^0\n"; 587} 588print STDOUT "N inline :$n\n"; 589 literal_data("mediawiki_revision: ".$commit{mw_revision}); 590print STDOUT "\n\n"; 591} 592 593# parse a sequence of 594# <cmd> <arg1> 595# <cmd> <arg2> 596# \n 597# (like batch sequence of import and sequence of push statements) 598sub get_more_refs { 599my$cmd=shift; 600my@refs; 601while(1) { 602my$line= <STDIN>; 603if($line=~m/^$cmd (.*)$/) { 604push(@refs,$1); 605}elsif($lineeq"\n") { 606return@refs; 607}else{ 608die("Invalid command in a '$cmd' batch: ".$_); 609} 610} 611} 612 613sub mw_import { 614# multiple import commands can follow each other. 615my@refs= (shift, get_more_refs("import")); 616foreachmy$ref(@refs) { 617 mw_import_ref($ref); 618} 619print STDOUT "done\n"; 620} 621 622sub mw_import_ref { 623my$ref=shift; 624# The remote helper will call "import HEAD" and 625# "import refs/heads/master". 626# Since HEAD is a symbolic ref to master (by convention, 627# followed by the output of the command "list" that we gave), 628# we don't need to do anything in this case. 629if($refeq"HEAD") { 630return; 631} 632 633 mw_connect_maybe(); 634 635my@pages= get_mw_pages(); 636 637print STDERR "Searching revisions...\n"; 638my$last_local= get_last_local_revision(); 639my$fetch_from=$last_local+1; 640if($fetch_from==1) { 641print STDERR ", fetching from beginning.\n"; 642}else{ 643print STDERR ", fetching from here.\n"; 644} 645my($n,@revisions) = fetch_mw_revisions(\@pages,$fetch_from); 646 647# Creation of the fast-import stream 648print STDERR "Fetching & writing export data...\n"; 649 650$n=0; 651my$last_timestamp=0;# Placeholer in case $rev->timestamp is undefined 652 653foreachmy$pagerevid(sort{$a->{revid} <=>$b->{revid}}@revisions) { 654# fetch the content of the pages 655my$query= { 656 action =>'query', 657 prop =>'revisions', 658 rvprop =>'content|timestamp|comment|user|ids', 659 revids =>$pagerevid->{revid}, 660}; 661 662my$result=$mediawiki->api($query); 663 664my$rev=pop(@{$result->{query}->{pages}->{$pagerevid->{pageid}}->{revisions}}); 665 666$n++; 667 668my%commit; 669$commit{author} =$rev->{user} ||'Anonymous'; 670$commit{comment} =$rev->{comment} ||'*Empty MediaWiki Message*'; 671$commit{title} = mediawiki_smudge_filename( 672$result->{query}->{pages}->{$pagerevid->{pageid}}->{title} 673); 674$commit{mw_revision} =$pagerevid->{revid}; 675$commit{content} = mediawiki_smudge($rev->{'*'}); 676 677if(!defined($rev->{timestamp})) { 678$last_timestamp++; 679}else{ 680$last_timestamp=$rev->{timestamp}; 681} 682$commit{date} = DateTime::Format::ISO8601->parse_datetime($last_timestamp); 683 684print STDERR "$n/",scalar(@revisions),": Revision #$pagerevid->{revid} of$commit{title}\n"; 685 686 import_file_revision(\%commit, ($fetch_from==1),$n); 687} 688 689if($fetch_from==1&&$n==0) { 690print STDERR "You appear to have cloned an empty MediaWiki.\n"; 691# Something has to be done remote-helper side. If nothing is done, an error is 692# thrown saying that HEAD is refering to unknown object 0000000000000000000 693# and the clone fails. 694} 695} 696 697sub error_non_fast_forward { 698my$advice= run_git("config --bool advice.pushNonFastForward"); 699chomp($advice); 700if($advicene"false") { 701# Native git-push would show this after the summary. 702# We can't ask it to display it cleanly, so print it 703# ourselves before. 704print STDERR "To prevent you from losing history, non-fast-forward updates were rejected\n"; 705print STDERR "Merge the remote changes (e.g. 'git pull') before pushing again. See the\n"; 706print STDERR "'Note about fast-forwards' section of 'git push --help' for details.\n"; 707} 708print STDOUT "error$_[0]\"non-fast-forward\"\n"; 709return0; 710} 711 712sub mw_upload_file { 713my$complete_file_name=shift; 714my$new_sha1=shift; 715my$extension=shift; 716my$file_deleted=shift; 717my$summary=shift; 718my$newrevid; 719my$path="File:".$complete_file_name; 720my%hashFiles= get_allowed_file_extensions(); 721if(!exists($hashFiles{$extension})) { 722print STDERR "$complete_file_nameis not a permitted file on this wiki.\n"; 723print STDERR "Check the configuration of file uploads in your mediawiki.\n"; 724return$newrevid; 725} 726# Deleting and uploading a file requires a priviledged user 727if($file_deleted) { 728 mw_connect_maybe(); 729my$query= { 730 action =>'delete', 731 title =>$path, 732 reason =>$summary 733}; 734if(!$mediawiki->edit($query)) { 735print STDERR "Failed to delete file on remote wiki\n"; 736print STDERR "Check your permissions on the remote site. Error code:\n"; 737print STDERR $mediawiki->{error}->{code} .':'.$mediawiki->{error}->{details}; 738exit1; 739} 740}else{ 741# Don't let perl try to interpret file content as UTF-8 => use "raw" 742my$content= run_git("cat-file blob$new_sha1","raw"); 743if($contentne"") { 744 mw_connect_maybe(); 745$mediawiki->{config}->{upload_url} = 746"$url/index.php/Special:Upload"; 747$mediawiki->edit({ 748 action =>'upload', 749 filename =>$complete_file_name, 750 comment =>$summary, 751 file => [undef, 752$complete_file_name, 753 Content =>$content], 754 ignorewarnings =>1, 755}, { 756 skip_encoding =>1 757} ) ||die$mediawiki->{error}->{code} .':' 758.$mediawiki->{error}->{details}; 759my$last_file_page=$mediawiki->get_page({title =>$path}); 760$newrevid=$last_file_page->{revid}; 761print STDERR "Pushed file:$new_sha1-$complete_file_name.\n"; 762}else{ 763print STDERR "Empty file$complete_file_namenot pushed.\n"; 764} 765} 766return$newrevid; 767} 768 769sub mw_push_file { 770my$diff_info=shift; 771# $diff_info contains a string in this format: 772# 100644 100644 <sha1_of_blob_before_commit> <sha1_of_blob_now> <status> 773my@diff_info_split=split(/[ \t]/,$diff_info); 774 775# Filename, including .mw extension 776my$complete_file_name=shift; 777# Commit message 778my$summary=shift; 779# MediaWiki revision number. Keep the previous one by default, 780# in case there's no edit to perform. 781my$oldrevid=shift; 782my$newrevid; 783 784my$new_sha1=$diff_info_split[3]; 785my$old_sha1=$diff_info_split[2]; 786my$page_created= ($old_sha1eq NULL_SHA1); 787my$page_deleted= ($new_sha1eq NULL_SHA1); 788$complete_file_name= mediawiki_clean_filename($complete_file_name); 789 790my($title,$extension) =$complete_file_name=~/^(.*)\.([^\.]*)$/; 791if(!defined($extension)) { 792$extension=""; 793} 794if($extensioneq"mw") { 795my$file_content; 796if($page_deleted) { 797# Deleting a page usually requires 798# special priviledges. A common 799# convention is to replace the page 800# with this content instead: 801$file_content= DELETED_CONTENT; 802}else{ 803$file_content= run_git("cat-file blob$new_sha1"); 804} 805 806 mw_connect_maybe(); 807 808my$result=$mediawiki->edit( { 809 action =>'edit', 810 summary =>$summary, 811 title =>$title, 812 basetimestamp =>$basetimestamps{$oldrevid}, 813 text => mediawiki_clean($file_content,$page_created), 814}, { 815 skip_encoding =>1# Helps with names with accentuated characters 816}); 817if(!$result) { 818if($mediawiki->{error}->{code} ==3) { 819# edit conflicts, considered as non-fast-forward 820print STDERR 'Warning: Error '. 821$mediawiki->{error}->{code} . 822' from mediwiki: '.$mediawiki->{error}->{details} . 823".\n"; 824return($oldrevid,"non-fast-forward"); 825}else{ 826# Other errors. Shouldn't happen => just die() 827die'Fatal: Error '. 828$mediawiki->{error}->{code} . 829' from mediwiki: '.$mediawiki->{error}->{details}; 830} 831} 832$newrevid=$result->{edit}->{newrevid}; 833print STDERR "Pushed file:$new_sha1-$title\n"; 834}else{ 835$newrevid= mw_upload_file($complete_file_name,$new_sha1, 836$extension,$page_deleted, 837$summary); 838} 839$newrevid= ($newrevidor$oldrevid); 840return($newrevid,"ok"); 841} 842 843sub mw_push { 844# multiple push statements can follow each other 845my@refsspecs= (shift, get_more_refs("push")); 846my$pushed; 847formy$refspec(@refsspecs) { 848my($force,$local,$remote) =$refspec=~/^(\+)?([^:]*):([^:]*)$/ 849or die("Invalid refspec for push. Expected <src>:<dst> or +<src>:<dst>"); 850if($force) { 851print STDERR "Warning: forced push not allowed on a MediaWiki.\n"; 852} 853if($localeq"") { 854print STDERR "Cannot delete remote branch on a MediaWiki\n"; 855print STDOUT "error$remotecannot delete\n"; 856next; 857} 858if($remotene"refs/heads/master") { 859print STDERR "Only push to the branch 'master' is supported on a MediaWiki\n"; 860print STDOUT "error$remoteonly master allowed\n"; 861next; 862} 863if(mw_push_revision($local,$remote)) { 864$pushed=1; 865} 866} 867 868# Notify Git that the push is done 869print STDOUT "\n"; 870 871if($pushed&&$dumb_push) { 872print STDERR "Just pushed some revisions to MediaWiki.\n"; 873print STDERR "The pushed revisions now have to be re-imported, and your current branch\n"; 874print STDERR "needs to be updated with these re-imported commits. You can do this with\n"; 875print STDERR "\n"; 876print STDERR " git pull --rebase\n"; 877print STDERR "\n"; 878} 879} 880 881sub mw_push_revision { 882my$local=shift; 883my$remote=shift;# actually, this has to be "refs/heads/master" at this point. 884my$last_local_revid= get_last_local_revision(); 885print STDERR ".\n";# Finish sentence started by get_last_local_revision() 886my$last_remote_revid= get_last_remote_revision(); 887my$mw_revision=$last_remote_revid; 888 889# Get sha1 of commit pointed by local HEAD 890my$HEAD_sha1= run_git("rev-parse$local2>/dev/null");chomp($HEAD_sha1); 891# Get sha1 of commit pointed by remotes/$remotename/master 892my$remoteorigin_sha1= run_git("rev-parse refs/remotes/$remotename/master2>/dev/null"); 893chomp($remoteorigin_sha1); 894 895if($last_local_revid>0&& 896$last_local_revid<$last_remote_revid) { 897return error_non_fast_forward($remote); 898} 899 900if($HEAD_sha1eq$remoteorigin_sha1) { 901# nothing to push 902return0; 903} 904 905# Get every commit in between HEAD and refs/remotes/origin/master, 906# including HEAD and refs/remotes/origin/master 907my@commit_pairs= (); 908if($last_local_revid>0) { 909my$parsed_sha1=$remoteorigin_sha1; 910# Find a path from last MediaWiki commit to pushed commit 911while($parsed_sha1ne$HEAD_sha1) { 912my@commit_info=grep(/^$parsed_sha1/,split(/\n/, run_git("rev-list --children$local"))); 913if(!@commit_info) { 914return error_non_fast_forward($remote); 915} 916my@commit_info_split=split(/ |\n/,$commit_info[0]); 917# $commit_info_split[1] is the sha1 of the commit to export 918# $commit_info_split[0] is the sha1 of its direct child 919push(@commit_pairs, \@commit_info_split); 920$parsed_sha1=$commit_info_split[1]; 921} 922}else{ 923# No remote mediawiki revision. Export the whole 924# history (linearized with --first-parent) 925print STDERR "Warning: no common ancestor, pushing complete history\n"; 926my$history= run_git("rev-list --first-parent --children$local"); 927my@history=split('\n',$history); 928@history=@history[1..$#history]; 929foreachmy$line(reverse@history) { 930my@commit_info_split=split(/ |\n/,$line); 931push(@commit_pairs, \@commit_info_split); 932} 933} 934 935foreachmy$commit_info_split(@commit_pairs) { 936my$sha1_child= @{$commit_info_split}[0]; 937my$sha1_commit= @{$commit_info_split}[1]; 938my$diff_infos= run_git("diff-tree -r --raw -z$sha1_child$sha1_commit"); 939# TODO: we could detect rename, and encode them with a #redirect on the wiki. 940# TODO: for now, it's just a delete+add 941my@diff_info_list=split(/\0/,$diff_infos); 942# Keep the subject line of the commit message as mediawiki comment for the revision 943my$commit_msg= run_git("log --no-walk --format=\"%s\"$sha1_commit"); 944chomp($commit_msg); 945# Push every blob 946while(@diff_info_list) { 947my$status; 948# git diff-tree -z gives an output like 949# <metadata>\0<filename1>\0 950# <metadata>\0<filename2>\0 951# and we've split on \0. 952my$info=shift(@diff_info_list); 953my$file=shift(@diff_info_list); 954($mw_revision,$status) = mw_push_file($info,$file,$commit_msg,$mw_revision); 955if($statuseq"non-fast-forward") { 956# we may already have sent part of the 957# commit to MediaWiki, but it's too 958# late to cancel it. Stop the push in 959# the middle, but still give an 960# accurate error message. 961return error_non_fast_forward($remote); 962} 963if($statusne"ok") { 964die("Unknown error from mw_push_file()"); 965} 966} 967unless($dumb_push) { 968 run_git("notes --ref=$remotename/mediawikiadd -m\"mediawiki_revision:$mw_revision\"$sha1_commit"); 969 run_git("update-ref -m\"Git-MediaWiki push\"refs/mediawiki/$remotename/master$sha1_commit$sha1_child"); 970} 971} 972 973print STDOUT "ok$remote\n"; 974return1; 975} 976 977sub get_allowed_file_extensions { 978 mw_connect_maybe(); 979 980my$query= { 981 action =>'query', 982 meta =>'siteinfo', 983 siprop =>'fileextensions' 984}; 985my$result=$mediawiki->api($query); 986my@file_extensions=map$_->{ext},@{$result->{query}->{fileextensions}}; 987my%hashFile=map{$_=>1}@file_extensions; 988 989return%hashFile; 990}