#!/usr/bin/perl # Copyright (C) 2013 # Benoit Person <benoit.person@ensimag.imag.fr> # Celestin Matte <celestin.matte@ensimag.imag.fr> # License: GPL v2 or later # Set of tools for git repo with a mediawiki remote. # Documentation & bugtracker: https://github.com/Git-Mediawiki/Git-Mediawiki use strict; use warnings; use Getopt::Long; use URI::URL qw(url); use LWP::UserAgent; use HTML::TreeBuilder; use Git; use MediaWiki::API; use Git::Mediawiki qw(clean_filename connect_maybe EMPTY HTTP_CODE_PAGE_NOT_FOUND); # By default, use UTF-8 to communicate with Git and the user binmode STDERR, ':encoding(UTF-8)'; binmode STDOUT, ':encoding(UTF-8)'; # Global parameters my $verbose = 0; sub v_print { if ($verbose) { return print {*STDERR} @_; } return; } # Preview parameters my $file_name = EMPTY; my $remote_name = EMPTY; my $preview_file_name = EMPTY; my $autoload = 0; sub file { $file_name = shift; return $file_name; } my %commands = ( 'help' => [\&help, {}, \&help], 'preview' => [\&preview, { '<>' => \&file, 'output|o=s' => \$preview_file_name, 'remote|r=s' => \$remote_name, 'autoload|a' => \$autoload }, \&preview_help] ); # Search for sub-command my $cmd = $commands{'help'}; for (0..@ARGV-1) { if (defined $commands{$ARGV[$_]}) { $cmd = $commands{$ARGV[$_]}; splice @ARGV, $_, 1; last; } }; GetOptions( %{$cmd->[1]}, 'help|h' => \&{$cmd->[2]}, 'verbose|v' => \$verbose); # Launch command &{$cmd->[0]}; ############################# Preview Functions ################################ sub preview_help { print {*STDOUT} <<'END'; USAGE: git mw preview [--remote|-r <remote name>] [--autoload|-a] [--output|-o <output filename>] [--verbose|-v] <blob> | <filename> DESCRIPTION: Preview is an utiliy to preview local content of a mediawiki repo as if it was pushed on the remote. For that, preview searches for the remote name of the current branch's upstream if --remote is not set. If that remote is not found or if it is not a mediawiki, it lists all mediawiki remotes configured and asks you to replay your command with the --remote option set properly. Then, it searches for a file named 'filename'. If it's not found in the current dir, it will assume it's a blob. The content retrieved in the file (or in the blob) will then be parsed by the remote mediawiki and combined with a template retrieved from the mediawiki. Finally, preview will save the HTML result in a file. and autoload it in your default web browser if the option --autoload is present. OPTIONS: -r <remote name>, --remote <remote name> If the remote is a mediawiki, the template and the parse engine used for the preview will be those of that remote. If not, a list of valid remotes will be shown. -a, --autoload Try to load the HTML output in a new tab (or new window) of your default web browser. -o <output filename>, --output <output filename> Change the HTML output filename. Default filename is based on the input filename with its extension replaced by '.html'. -v, --verbose Show more information on what's going on under the hood. END exit; } sub preview { my $wiki; my ($remote_url, $wiki_page_name); my ($new_content, $template); my $file_content; if ($file_name eq EMPTY) { die "Missing file argument, see `git mw help`\n"; } v_print("### Selecting remote\n"); if ($remote_name eq EMPTY) { $remote_name = find_upstream_remote_name(); if ($remote_name) { $remote_url = mediawiki_remote_url_maybe($remote_name); } if (! $remote_url) { my @valid_remotes = find_mediawiki_remotes(); if ($#valid_remotes == 0) { print {*STDERR} "No mediawiki remote in this repo. \n"; exit 1; } else { my $remotes_list = join("\n\t", @valid_remotes); print {*STDERR} <<"MESSAGE"; There are multiple mediawiki remotes, which of: ${remotes_list} do you want ? Use the -r option to specify the remote. MESSAGE } exit 1; } } else { if (!is_valid_remote($remote_name)) { die "${remote_name} is not a remote\n"; } $remote_url = mediawiki_remote_url_maybe($remote_name); if (! $remote_url) { die "${remote_name} is not a mediawiki remote\n"; } } v_print("selected remote:\n\tname: ${remote_name}\n\turl: ${remote_url}\n"); $wiki = connect_maybe($wiki, $remote_name, $remote_url); # Read file content if (! -e $file_name) { $file_content = git_cmd_try { Git::command('cat-file', 'blob', $file_name); } "%s failed w/ code %d"; if ($file_name =~ /(.+):(.+)/) { $file_name = $2; } } else { open my $read_fh, "<", $file_name or die "could not open ${file_name}: $!\n"; $file_content = do { local $/ = undef; <$read_fh> }; close $read_fh or die "unable to close: $!\n"; } v_print("### Retrieving template\n"); ($wiki_page_name = clean_filename($file_name)) =~ s/\.[^.]+$//; $template = get_template($remote_url, $wiki_page_name); v_print("### Parsing local content\n"); $new_content = $wiki->api({ action => 'parse', text => $file_content, title => $wiki_page_name }, { skip_encoding => 1 }) or die "No response from remote mediawiki\n"; $new_content = $new_content->{'parse'}->{'text'}->{'*'}; v_print("### Merging contents\n"); if ($preview_file_name eq EMPTY) { ($preview_file_name = $file_name) =~ s/\.[^.]+$/.html/; } open(my $save_fh, '>:encoding(UTF-8)', $preview_file_name) or die "Could not open: $!\n"; print {$save_fh} merge_contents($template, $new_content, $remote_url); close($save_fh) or die "Could not close: $!\n"; v_print("### Results\n"); if ($autoload) { v_print("Launching browser w/ file: ${preview_file_name}"); system('git', 'web--browse', $preview_file_name); } else { print {*STDERR} "Preview file saved as: ${preview_file_name}\n"; } exit; } # uses global scope variable: $remote_name sub merge_contents { my $template = shift; my $content = shift; my $remote_url = shift; my ($content_tree, $html_tree, $mw_content_text); my $template_content_id = 'bodyContent'; $html_tree = HTML::TreeBuilder->new; $html_tree->parse($template); $content_tree = HTML::TreeBuilder->new; $content_tree->parse($content); $template_content_id = Git::config("remote.${remote_name}.mwIDcontent") || $template_content_id; v_print("Using '${template_content_id}' as the content ID\n"); $mw_content_text = $html_tree->look_down('id', $template_content_id); if (!defined $mw_content_text) { print {*STDERR} <<"CONFIG"; Could not combine the new content with the template. You might want to configure `mediawiki.IDContent` in your config: git config --add remote.${remote_name}.mwIDcontent <id> and re-run the command afterward. CONFIG exit 1; } $mw_content_text->delete_content(); $mw_content_text->push_content($content_tree); make_links_absolute($html_tree, $remote_url); return $html_tree->as_HTML; } sub make_links_absolute { my $html_tree = shift; my $remote_url = shift; for (@{ $html_tree->extract_links() }) { my ($link, $element, $attr) = @{ $_ }; my $url = url($link)->canonical; if ($url !~ /#/) { $element->attr($attr, URI->new_abs($url, $remote_url)); } } return $html_tree; } sub is_valid_remote { my $remote = shift; my @remotes = git_cmd_try { Git::command('remote') } "%s failed w/ code %d"; my $found_remote = 0; foreach my $remote (@remotes) { if ($remote eq $remote) { $found_remote = 1; last; } } return $found_remote; } sub find_mediawiki_remotes { my @remotes = git_cmd_try { Git::command('remote'); } "%s failed w/ code %d"; my $remote_url; my @valid_remotes = (); foreach my $remote (@remotes) { $remote_url = mediawiki_remote_url_maybe($remote); if ($remote_url) { push(@valid_remotes, $remote); } } return @valid_remotes; } sub find_upstream_remote_name { my $current_branch = git_cmd_try { Git::command_oneline('symbolic-ref', '--short', 'HEAD') } "%s failed w/ code %d"; return Git::config("branch.${current_branch}.remote"); } sub mediawiki_remote_url_maybe { my $remote = shift; # Find remote url my $remote_url = Git::config("remote.${remote}.url"); if ($remote_url =~ s/mediawiki::(.*)/$1/) { return url($remote_url)->canonical; } return; } sub get_template { my $url = shift; my $page_name = shift; my ($req, $res, $code, $url_after); $req = LWP::UserAgent->new; if ($verbose) { $req->show_progress(1); } $res = $req->get("${url}/index.php?title=${page_name}"); if (!$res->is_success) { $code = $res->code; $url_after = $res->request()->uri(); # resolve all redirections if ($code == HTTP_CODE_PAGE_NOT_FOUND) { if ($verbose) { print {*STDERR} <<"WARNING"; Warning: Failed to retrieve '$page_name'. Create it on the mediawiki if you want all the links to work properly. Trying to use the mediawiki homepage as a fallback template ... WARNING } # LWP automatically redirects GET request $res = $req->get("${url}/index.php"); if (!$res->is_success) { $url_after = $res->request()->uri(); # resolve all redirections die "Failed to get homepage @ ${url_after} w/ code ${code}\n"; } } else { die "Failed to get '${page_name}' @ ${url_after} w/ code ${code}\n"; } } return $res->decoded_content; } ############################## Help Functions ################################## sub help { print {*STDOUT} <<'END'; usage: git mw <command> <args> git mw commands are: help Display help information about git mw preview Parse and render local file into HTML END exit; }