From 65d1dda5c1e79aa9c85a2d16bdd1a12413f163af Mon Sep 17 00:00:00 2001
From: Ian Kelling
Date: Mon, 8 May 2023 18:29:58 -0400
Subject: [PATCH] various fixes, a few new features
---
brc | 8 +-
brc2 | 11 +
btrbk-run | 45 +-
btrfsmaint | 23 +-
conflink | 24 +-
distro-end | 8 +-
filesystem/usr/share/gitweb/gitweb.cgi | 8418 -----------------
laptop-btrbk | 13 +-
.../etc/systemd/system/btrbk-spread.service | 10 +
.../etc/systemd/system/btrbk-spread.timer | 8 +
mail-setup | 7 +
system-status | 39 +-
12 files changed, 141 insertions(+), 8473 deletions(-)
delete mode 100755 filesystem/usr/share/gitweb/gitweb.cgi
create mode 100644 machine_specific/kd/filesystem/etc/systemd/system/btrbk-spread.service
create mode 100644 machine_specific/kd/filesystem/etc/systemd/system/btrbk-spread.timer
diff --git a/brc b/brc
index 7d58392..1539a76 100644
--- a/brc
+++ b/brc
@@ -1013,7 +1013,7 @@ ccomp tail etail etail2
showkeys() {
ssh "$@" cat .ssh/authorized_keys{,2}
- }
+}
# print exim old pids
@@ -2573,6 +2573,10 @@ if [[ $- == *i* ]]; then
if [[ $MAIL_HOST && $MAIL_HOST != "$HOSTNAME" ]]; then
ps_char="@ $ps_char"
fi
+ jobs_char=
+ if [[ $(jobs -p) ]]; then
+ jobs_char='\j '
+ fi
# We could test if sudo is active with sudo -nv
# but then we get an email and log of lots of failed sudo commands.
# We could turn those off, but seems better not to.
@@ -2582,7 +2586,7 @@ if [[ $- == *i* ]]; then
if [[ ! $HISTFILE ]]; then
ps_char="NOHIST $ps_char"
fi
- PS1="${PS1%"${PS1#*[wW]}"} $psudo\[$ps_color\]$ps_char\[$term_nocolor\] "
+ PS1="${PS1%"${PS1#*[wW]}"} $jobs_char$psudo\[$ps_color\]$ps_char\[$term_nocolor\] "
# set titlebar. instead, using more advanced
# titelbar below
diff --git a/brc2 b/brc2
index 676c597..c747109 100644
--- a/brc2
+++ b/brc2
@@ -3159,6 +3159,17 @@ fixu() {
fi
}
+# unmute
+um() {
+ pactl set-sink-mute @DEFAULT_SINK@ false
+ rm -f /tmp/ianknap
+}
+nap() {
+ pactl set-sink-mute @DEFAULT_SINK@ true
+ touch /tmp/ianknap
+}
+
+
# systemctl is-enabled / status / cat says nothing, instead theres
# some obscure symlink. paths copied from man systemd.unit.
# possibly also usefull, but incomplete, doesnt show units not loaded in memory:
diff --git a/btrbk-run b/btrbk-run
index cf7cf83..cec50f1 100644
--- a/btrbk-run
+++ b/btrbk-run
@@ -89,8 +89,9 @@ targets=()
early=false
cron=false
fast=false
+kd_spread_maybe=false
orig_args=("$@")
-temp=$(getopt -l cron,fast,pull-reexec,help 23ceil:m:npqrs:t:vh "$@") || usage 1
+temp=$(getopt -l cron,fast,pull-reexec,help 23ceikl:m:npqrs:t:vh "$@") || usage 1
eval set -- "$temp"
while true; do
case $1 in
@@ -113,6 +114,8 @@ while true; do
# switch mail-host, no need to repeat the same checks again.
--fast) fast=true ;;
-i) incremental_strict=true ;;
+ # note this implies resume
+ -k) kd_spread_maybe=true ;;
# bytes per second, suffix k m g
-l) rate_limit=$2; shift ;;
# Comma separated mountpoints to backup. This has defaults set below.
@@ -145,6 +148,14 @@ while true; do
shift
done
+if $kd_spread_maybe; then
+ if [[ $1 && $1 != resume ]]; then
+ die "dont pass -k without resume or empty run arg"
+ fi
+ cmd_arg=resume
+fi
+
+
cmd_arg=${1:-run}
@@ -180,9 +191,6 @@ if [[ -v targets && $source ]]; then
die "error: -t and -s are mutually exclusive"
fi
-if $verbose; then
- printf "$pre options: conf_only=%s\ndry_run=%s\nrate_limit=%s\nverbose=%s\ncmd_arg=%s" "$conf_only" "$dry_run" "$rate_limit" "$verbose" "$cmd_arg"
-fi
### end options parsing
# remove path from earlier version of btrbk
@@ -207,7 +215,7 @@ kd_spread=false
if [[ ! -v targets && ! $source ]]; then
if $cron; then
if [[ $HOSTNAME != "$MAIL_HOST" ]]; then
- if [[ $HOSTNAME == kd && $MAIL_HOST == x3 ]]; then
+ if $kd_spread_maybe && [[ $HOSTNAME == kd && $MAIL_HOST == x3 ]]; then
if ping -q -c1 -w1 x3.office.fsf.org &>/dev/null; then
work_host=x3.office.fsf.org
elif ping -q -c1 -w1 x3wg.b8.nz &>/dev/null; then
@@ -311,6 +319,11 @@ if [[ ! -v targets && ! $source ]]; then
esac
fi
+if $verbose; then
+ printf "$pre options: conf_only=%s\ndry_run=%s\nrate_limit=%s\nverbose=%s\ncmd_arg=%s" "$conf_only" "$dry_run" "$rate_limit" "$verbose" "$cmd_arg"
+fi
+
+
if [[ -v targets ]]; then
echo "targets: ${targets[*]}"
fi
@@ -447,7 +460,6 @@ else
sshable=()
sshfail=()
- min_idle_ms=$((1000 * 60 * 15))
for h in ${targets[@]}; do
if $fast || $conf_only; then
# Use some typical values in this case
@@ -484,15 +496,26 @@ else
die "error: filesystem on target $h is $percent_used % full"
fi
- # This is a separate ssh because xprintidle can fail and thats ok.
- if $cron && idle_ms=$(timeout -s 9 6 ssh $h DISPLAY=:0 xprintidle); then
- if (( idle_ms < min_idle_ms )); then
-
+ # on sy, xprintidle is resetting every 12 seconds even when not
+ # idle, i dunno why, instead we are checking if the screen is locked,
+ # which is good enough.
+ #
+ # This is a separate ssh because the command can fail and thatis ok.
+ if $cron; then
+ locked=false
+ if lock_info=$(timeout -s 9 6 ssh $h DISPLAY=:0 xscreensaver-command -timer); then
+ if [[ $lock_info != *non-blanked* ]]; then
+ locked=true
+ fi
+ else
+ locked=true
+ fi
+ if $locked; then
# Ignore this host. i sometimes use a non-main machine for
# testing or web browsing, knowing that everything will be wiped
# by the next backup, but I dont want it to happen as Im using
# it from cronjob.
- e "warning: $h: active X session in the last 15 minutes, skipping for now"
+ e "warning: $h: seems to be actively in use, skipping for now"
continue
fi
fi
diff --git a/btrfsmaint b/btrfsmaint
index 0204ed5..7b2dabe 100755
--- a/btrfsmaint
+++ b/btrfsmaint
@@ -32,21 +32,15 @@ e() {
}
check-idle() {
- type -p xprintidle &>/dev/null || return 0
+ type -p xscreensaver-command &>/dev/null || return 0
export DISPLAY=:0
- # a hours, a movie could run that long.
- idle_limit=$((1000 * 60 * 60 * 2))
- idle_time=$idle_limit
- while read -r user; do
- new_idle_time=$(sudo -u $user xprintidle 2>/dev/null) ||:
- if [[ $new_idle_time && $new_idle_time -lt $idle_time ]]; then
- idle_time=$new_idle_time
+ locked=false
+ if lock_info=$(xscreensaver-command -timer); then
+ if [[ $lock_info != *non-blanked* ]]; then
+ locked=true
fi
- done < <(users | tr " " "\n" | sort -u)
- if (( idle_time < idle_limit )); then
- idle=false
else
- idle=true
+ locked=true
fi
}
@@ -107,7 +101,6 @@ readonly check dryrun force stats
main() {
- idle=true
if ! $force; then
check-idle
if ! $check; then
@@ -115,7 +108,7 @@ main() {
max_min=300
# When the cron kicks in, we may not be idle (physically sleeping) yet, so
# wait.
- while ! $idle && (( min < max_min )); do
+ while ! $locked && (( min < max_min )); do
min=$(( min + 1 ))
sleep 60
check-idle
@@ -161,7 +154,7 @@ EOF
#### end look for diff in stats, eg: increasing error count ####
if $check; then
- if ! $idle; then
+ if ! $locked; then
if $dryrun; then
echo "$0: not idle. if this wasnt a dry run, btrfs scrub cancel $mnt"
else
diff --git a/conflink b/conflink
index 56a251f..5c135c4 100755
--- a/conflink
+++ b/conflink
@@ -104,7 +104,6 @@ subdir-link-r() {
common-file-setup() {
local dir fs x f reload_systemd
- local -a reload_services
local -a restart_services
reload_systemd=false
# note, i ran chmod -R g-s on the filesystem dirs
@@ -234,27 +233,14 @@ case $user in
if [[ -e /var/lib/znc ]] && getent group znc; then
s chown -R znc:znc /var/lib/znc
fi
- for f in /etc/prometheus-{,export-}htpasswd; do
- if [[ -e $f ]]; then
- # note: this is duplicative of the file's own permissions
- s chmod 640 $f
- if getent passwd www-data &>/dev/null; then
- s chown root:www-data $f
- fi
- fi
- done
- f=/etc/prometheus-pass
- if [[ -e $f ]]; then
- # note: this is duplicative of the file's own permissions
- s chmod 640 $f
- if getent passwd prometheus &>/dev/null; then
- s chown root:prometheus $f
- fi
+ if getent passwd prometheus &>/dev/null; then
+ s rsync -clpgoDiSAX --chmod=Dg-s --chown=root:prometheus /p/c/user-specific/prometheus/prometheus-pass /etc
+ fi
+ if getent passwd www-data &>/dev/null; then
+ s rsync -clpgoDiSAX --chmod=Dg-s --chown=root:www-data /p/c/user-specific/prometheus-web/prometheus-htpasswd /etc
fi
f=/etc/nagios4/htdigest.users
if [[ -e $f ]]; then
- # note: this is duplicative of the file's own permissions
- s chmod 640 $f /etc/prometheus-pass
if getent passwd nagios &>/dev/null; then
s chown nagios:www-data $f
fi
diff --git a/distro-end b/distro-end
index a9ab55a..6b86c12 100755
--- a/distro-end
+++ b/distro-end
@@ -886,10 +886,12 @@ esac
case $HOSTNAME in
bk)
+ pi icecast2
+ # todo, save the config
/etc/cron.daily/stream-cert
web-conf -c /etc/cert-live.fsf.org -p 443 -f 8000 apache2 live.fsf.org
;;
- esac
+esac
###### end website setup
@@ -1957,6 +1959,10 @@ sgo dynamicipupdate
if grep -xFq $HOSTNAME /a/bin/ds/machine_specific/btrbk.hosts; then
sgo btrbk.timer
fi
+if [[ $HOSTNAME == kd ]]; then
+ sgo btrbk-spread.timer
+fi
+
# note: to see when it was last run,
# ser list-timers
diff --git a/filesystem/usr/share/gitweb/gitweb.cgi b/filesystem/usr/share/gitweb/gitweb.cgi
deleted file mode 100755
index ea4250f..0000000
--- a/filesystem/usr/share/gitweb/gitweb.cgi
+++ /dev/null
@@ -1,8418 +0,0 @@
-#!/usr/bin/perl
-
-# gitweb - simple web interface to track changes in git repositories
-#
-# (C) 2005-2006, Kay Sievers
-# (C) 2005, Christian Gierke
-#
-# This program is licensed under the GPLv2
-
-use 5.008;
-use strict;
-use warnings;
-use CGI qw(:standard :escapeHTML -nosticky);
-use CGI::Util qw(unescape);
-use CGI::Carp qw(fatalsToBrowser set_message);
-use Encode;
-use Fcntl ':mode';
-use File::Find qw();
-use File::Basename qw(basename);
-use Time::HiRes qw(gettimeofday tv_interval);
-binmode STDOUT, ':utf8';
-
-if (!defined($CGI::VERSION) || $CGI::VERSION < 4.08) {
- eval 'sub CGI::multi_param { CGI::param(@_) }'
-}
-
-our $t0 = [ gettimeofday() ];
-our $number_of_git_cmds = 0;
-
-BEGIN {
- CGI->compile() if $ENV{'MOD_PERL'};
-}
-
-our $version = "2.8.1";
-
-our ($my_url, $my_uri, $base_url, $path_info, $home_link);
-sub evaluate_uri {
- our $cgi;
-
- our $my_url = $cgi->url();
- our $my_uri = $cgi->url(-absolute => 1);
-
- # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
- # needed and used only for URLs with nonempty PATH_INFO
- our $base_url = $my_url;
-
- # When the script is used as DirectoryIndex, the URL does not contain the name
- # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
- # have to do it ourselves. We make $path_info global because it's also used
- # later on.
- #
- # Another issue with the script being the DirectoryIndex is that the resulting
- # $my_url data is not the full script URL: this is good, because we want
- # generated links to keep implying the script name if it wasn't explicitly
- # indicated in the URL we're handling, but it means that $my_url cannot be used
- # as base URL.
- # Therefore, if we needed to strip PATH_INFO, then we know that we have
- # to build the base URL ourselves:
- our $path_info = decode_utf8($ENV{"PATH_INFO"});
- if ($path_info) {
- # $path_info has already been URL-decoded by the web server, but
- # $my_url and $my_uri have not. URL-decode them so we can properly
- # strip $path_info.
- $my_url = unescape($my_url);
- $my_uri = unescape($my_uri);
- if ($my_url =~ s,\Q$path_info\E$,, &&
- $my_uri =~ s,\Q$path_info\E$,, &&
- defined $ENV{'SCRIPT_NAME'}) {
- $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
- }
- }
-
- # target of the home link on top of all pages
- our $home_link = $my_uri || "/";
-}
-
-# core git executable to use
-# this can just be "git" if your webserver has a sensible PATH
-our $GIT = "/usr/bin/git";
-
-# absolute fs-path which will be prepended to the project path
-#our $projectroot = "/pub/scm";
-our $projectroot = "/pub/git";
-
-# fs traversing limit for getting project list
-# the number is relative to the projectroot
-our $project_maxdepth = 2007;
-
-# string of the home link on top of all pages
-our $home_link_str = "projects";
-
-# extra breadcrumbs preceding the home link
-our @extra_breadcrumbs = ();
-
-# name of your site or organization to appear in page titles
-# replace this with something more descriptive for clearer bookmarks
-our $site_name = ""
- || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
-
-# html snippet to include in the section of each page
-our $site_html_head_string = "";
-# filename of html text to include at top of each page
-our $site_header = "";
-# html text to include at home page
-our $home_text = "indextext.html";
-# filename of html text to include at bottom of each page
-our $site_footer = "";
-
-# URI of stylesheets
-our @stylesheets = ("static/gitweb.css");
-# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
-our $stylesheet = undef;
-# URI of GIT logo (72x27 size)
-our $logo = "static/git-logo.png";
-# URI of GIT favicon, assumed to be image/png type
-our $favicon = "static/git-favicon.png";
-# URI of gitweb.js (JavaScript code for gitweb)
-our $javascript = "static/gitweb.js";
-
-# URI and label (title) of GIT logo link
-#our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
-#our $logo_label = "git documentation";
-our $logo_url = "http://git-scm.com/";
-our $logo_label = "git homepage";
-
-# source of projects list
-our $projects_list = "";
-
-# the width (in characters) of the projects list "Description" column
-our $projects_list_description_width = 25;
-
-# group projects by category on the projects list
-# (enabled if this variable evaluates to true)
-our $projects_list_group_categories = 0;
-
-# default category if none specified
-# (leave the empty string for no category)
-our $project_list_default_category = "";
-
-# default order of projects list
-# valid values are none, project, descr, owner, and age
-our $default_projects_order = "project";
-
-# show repository only if this file exists
-# (only effective if this variable evaluates to true)
-our $export_ok = "";
-
-# don't generate age column on the projects list page
-our $omit_age_column = 0;
-
-# don't generate information about owners of repositories
-our $omit_owner=0;
-
-# show repository only if this subroutine returns true
-# when given the path to the project, for example:
-# sub { return -e "$_[0]/git-daemon-export-ok"; }
-our $export_auth_hook = undef;
-
-# only allow viewing of repositories also shown on the overview page
-our $strict_export = "";
-
-# list of git base URLs used for URL to where fetch project from,
-# i.e. full URL is "$git_base_url/$project"
-our @git_base_url_list = grep { $_ ne '' } ("");
-
-# default blob_plain mimetype and default charset for text/plain blob
-our $default_blob_plain_mimetype = 'text/plain';
-our $default_text_plain_charset = undef;
-
-# file to use for guessing MIME types before trying /etc/mime.types
-# (relative to the current git repository)
-our $mimetypes_file = undef;
-
-# assume this charset if line contains non-UTF-8 characters;
-# it should be valid encoding (see Encoding::Supported(3pm) for list),
-# for which encoding all byte sequences are valid, for example
-# 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
-# could be even 'utf-8' for the old behavior)
-our $fallback_encoding = 'latin1';
-
-# rename detection options for git-diff and git-diff-tree
-# - default is '-M', with the cost proportional to
-# (number of removed files) * (number of new files).
-# - more costly is '-C' (which implies '-M'), with the cost proportional to
-# (number of changed files + number of removed files) * (number of new files)
-# - even more costly is '-C', '--find-copies-harder' with cost
-# (number of files in the original tree) * (number of new files)
-# - one might want to include '-B' option, e.g. '-B', '-M'
-our @diff_opts = ('-M'); # taken from git_commit
-
-# Disables features that would allow repository owners to inject script into
-# the gitweb domain.
-our $prevent_xss = 0;
-
-# Path to the highlight executable to use (must be the one from
-# http://www.andre-simon.de due to assumptions about parameters and output).
-# Useful if highlight is not installed on your webserver's PATH.
-# [Default: highlight]
-our $highlight_bin = "highlight";
-
-# information about snapshot formats that gitweb is capable of serving
-our %known_snapshot_formats = (
- # name => {
- # 'display' => display name,
- # 'type' => mime type,
- # 'suffix' => filename suffix,
- # 'format' => --format for git-archive,
- # 'compressor' => [compressor command and arguments]
- # (array reference, optional)
- # 'disabled' => boolean (optional)}
- #
- 'tgz' => {
- 'display' => 'tar.gz',
- 'type' => 'application/x-gzip',
- 'suffix' => '.tar.gz',
- 'format' => 'tar',
- 'compressor' => ['gzip', '-n']},
-
- 'tbz2' => {
- 'display' => 'tar.bz2',
- 'type' => 'application/x-bzip2',
- 'suffix' => '.tar.bz2',
- 'format' => 'tar',
- 'compressor' => ['bzip2']},
-
- 'txz' => {
- 'display' => 'tar.xz',
- 'type' => 'application/x-xz',
- 'suffix' => '.tar.xz',
- 'format' => 'tar',
- 'compressor' => ['xz'],
- 'disabled' => 1},
-
- 'zip' => {
- 'display' => 'zip',
- 'type' => 'application/x-zip',
- 'suffix' => '.zip',
- 'format' => 'zip'},
-);
-
-# Aliases so we understand old gitweb.snapshot values in repository
-# configuration.
-our %known_snapshot_format_aliases = (
- 'gzip' => 'tgz',
- 'bzip2' => 'tbz2',
- 'xz' => 'txz',
-
- # backward compatibility: legacy gitweb config support
- 'x-gzip' => undef, 'gz' => undef,
- 'x-bzip2' => undef, 'bz2' => undef,
- 'x-zip' => undef, '' => undef,
-);
-
-# Pixel sizes for icons and avatars. If the default font sizes or lineheights
-# are changed, it may be appropriate to change these values too via
-# $GITWEB_CONFIG.
-our %avatar_size = (
- 'default' => 16,
- 'double' => 32
-);
-
-# Used to set the maximum load that we will still respond to gitweb queries.
-# If server load exceed this value then return "503 server busy" error.
-# If gitweb cannot determined server load, it is taken to be 0.
-# Leave it undefined (or set to 'undef') to turn off load checking.
-our $maxload = 300;
-
-# configuration for 'highlight' (http://www.andre-simon.de/)
-# match by basename
-our %highlight_basename = (
- #'Program' => 'py',
- #'Library' => 'py',
- 'SConstruct' => 'py', # SCons equivalent of Makefile
- 'Makefile' => 'make',
-);
-# match by extension
-our %highlight_ext = (
- # main extensions, defining name of syntax;
- # see files in /usr/share/highlight/langDefs/ directory
- (map { $_ => $_ } qw(py rb java css js tex bib xml awk bat ini spec tcl sql)),
- # alternate extensions, see /etc/highlight/filetypes.conf
- (map { $_ => 'c' } qw(c h)),
- (map { $_ => 'sh' } qw(sh bash zsh ksh)),
- (map { $_ => 'cpp' } qw(cpp cxx c++ cc)),
- (map { $_ => 'php' } qw(php php3 php4 php5 phps)),
- (map { $_ => 'pl' } qw(pl perl pm)), # perhaps also 'cgi'
- (map { $_ => 'make'} qw(make mak mk)),
- (map { $_ => 'xml' } qw(xml xhtml html htm)),
-);
-
-# You define site-wide feature defaults here; override them with
-# $GITWEB_CONFIG as necessary.
-our %feature = (
- # feature => {
- # 'sub' => feature-sub (subroutine),
- # 'override' => allow-override (boolean),
- # 'default' => [ default options...] (array reference)}
- #
- # if feature is overridable (it means that allow-override has true value),
- # then feature-sub will be called with default options as parameters;
- # return value of feature-sub indicates if to enable specified feature
- #
- # if there is no 'sub' key (no feature-sub), then feature cannot be
- # overridden
- #
- # use gitweb_get_feature() to retrieve the value
- # (an array) or gitweb_check_feature() to check if
- # is enabled
-
- # Enable the 'blame' blob view, showing the last commit that modified
- # each line in the file. This can be very CPU-intensive.
-
- # To enable system wide have in $GITWEB_CONFIG
- # $feature{'blame'}{'default'} = [1];
- # To have project specific config enable override in $GITWEB_CONFIG
- # $feature{'blame'}{'override'} = 1;
- # and in project config gitweb.blame = 0|1;
- 'blame' => {
- 'sub' => sub { feature_bool('blame', @_) },
- 'override' => 0,
- 'default' => [0]},
-
- # Enable the 'snapshot' link, providing a compressed archive of any
- # tree. This can potentially generate high traffic if you have large
- # project.
-
- # Value is a list of formats defined in %known_snapshot_formats that
- # you wish to offer.
- # To disable system wide have in $GITWEB_CONFIG
- # $feature{'snapshot'}{'default'} = [];
- # To have project specific config enable override in $GITWEB_CONFIG
- # $feature{'snapshot'}{'override'} = 1;
- # and in project config, a comma-separated list of formats or "none"
- # to disable. Example: gitweb.snapshot = tbz2,zip;
- 'snapshot' => {
- 'sub' => \&feature_snapshot,
- 'override' => 0,
- 'default' => ['tgz']},
-
- # Enable text search, which will list the commits which match author,
- # committer or commit text to a given string. Enabled by default.
- # Project specific override is not supported.
- #
- # Note that this controls all search features, which means that if
- # it is disabled, then 'grep' and 'pickaxe' search would also be
- # disabled.
- 'search' => {
- 'override' => 0,
- 'default' => [1]},
-
- # Enable grep search, which will list the files in currently selected
- # tree containing the given string. Enabled by default. This can be
- # potentially CPU-intensive, of course.
- # Note that you need to have 'search' feature enabled too.
-
- # To enable system wide have in $GITWEB_CONFIG
- # $feature{'grep'}{'default'} = [1];
- # To have project specific config enable override in $GITWEB_CONFIG
- # $feature{'grep'}{'override'} = 1;
- # and in project config gitweb.grep = 0|1;
- 'grep' => {
- 'sub' => sub { feature_bool('grep', @_) },
- 'override' => 0,
- 'default' => [1]},
-
- # Enable the pickaxe search, which will list the commits that modified
- # a given string in a file. This can be practical and quite faster
- # alternative to 'blame', but still potentially CPU-intensive.
- # Note that you need to have 'search' feature enabled too.
-
- # To enable system wide have in $GITWEB_CONFIG
- # $feature{'pickaxe'}{'default'} = [1];
- # To have project specific config enable override in $GITWEB_CONFIG
- # $feature{'pickaxe'}{'override'} = 1;
- # and in project config gitweb.pickaxe = 0|1;
- 'pickaxe' => {
- 'sub' => sub { feature_bool('pickaxe', @_) },
- 'override' => 0,
- 'default' => [1]},
-
- # Enable showing size of blobs in a 'tree' view, in a separate
- # column, similar to what 'ls -l' does. This cost a bit of IO.
-
- # To disable system wide have in $GITWEB_CONFIG
- # $feature{'show-sizes'}{'default'} = [0];
- # To have project specific config enable override in $GITWEB_CONFIG
- # $feature{'show-sizes'}{'override'} = 1;
- # and in project config gitweb.showsizes = 0|1;
- 'show-sizes' => {
- 'sub' => sub { feature_bool('showsizes', @_) },
- 'override' => 0,
- 'default' => [1]},
-
- # Make gitweb use an alternative format of the URLs which can be
- # more readable and natural-looking: project name is embedded
- # directly in the path and the query string contains other
- # auxiliary information. All gitweb installations recognize
- # URL in either format; this configures in which formats gitweb
- # generates links.
-
- # To enable system wide have in $GITWEB_CONFIG
- # $feature{'pathinfo'}{'default'} = [1];
- # Project specific override is not supported.
-
- # Note that you will need to change the default location of CSS,
- # favicon, logo and possibly other files to an absolute URL. Also,
- # if gitweb.cgi serves as your indexfile, you will need to force
- # $my_uri to contain the script name in your $GITWEB_CONFIG.
- 'pathinfo' => {
- 'override' => 0,
- 'default' => [0]},
-
- # Make gitweb consider projects in project root subdirectories
- # to be forks of existing projects. Given project $projname.git,
- # projects matching $projname/*.git will not be shown in the main
- # projects list, instead a '+' mark will be added to $projname
- # there and a 'forks' view will be enabled for the project, listing
- # all the forks. If project list is taken from a file, forks have
- # to be listed after the main project.
-
- # To enable system wide have in $GITWEB_CONFIG
- # $feature{'forks'}{'default'} = [1];
- # Project specific override is not supported.
- 'forks' => {
- 'override' => 0,
- 'default' => [0]},
-
- # Insert custom links to the action bar of all project pages.
- # This enables you mainly to link to third-party scripts integrating
- # into gitweb; e.g. git-browser for graphical history representation
- # or custom web-based repository administration interface.
-
- # The 'default' value consists of a list of triplets in the form
- # (label, link, position) where position is the label after which
- # to insert the link and link is a format string where %n expands
- # to the project name, %f to the project path within the filesystem,
- # %h to the current hash (h gitweb parameter) and %b to the current
- # hash base (hb gitweb parameter); %% expands to %.
-
- # To enable system wide have in $GITWEB_CONFIG e.g.
- # $feature{'actions'}{'default'} = [('graphiclog',
- # '/git-browser/by-commit.html?r=%n', 'summary')];
- # Project specific override is not supported.
- 'actions' => {
- 'override' => 0,
- 'default' => []},
-
- # Allow gitweb scan project content tags of project repository,
- # and display the popular Web 2.0-ish "tag cloud" near the projects
- # list. Note that this is something COMPLETELY different from the
- # normal Git tags.
-
- # gitweb by itself can show existing tags, but it does not handle
- # tagging itself; you need to do it externally, outside gitweb.
- # The format is described in git_get_project_ctags() subroutine.
- # You may want to install the HTML::TagCloud Perl module to get
- # a pretty tag cloud instead of just a list of tags.
-
- # To enable system wide have in $GITWEB_CONFIG
- # $feature{'ctags'}{'default'} = [1];
- # Project specific override is not supported.
-
- # In the future whether ctags editing is enabled might depend
- # on the value, but using 1 should always mean no editing of ctags.
- 'ctags' => {
- 'override' => 0,
- 'default' => [0]},
-
- # The maximum number of patches in a patchset generated in patch
- # view. Set this to 0 or undef to disable patch view, or to a
- # negative number to remove any limit.
-
- # To disable system wide have in $GITWEB_CONFIG
- # $feature{'patches'}{'default'} = [0];
- # To have project specific config enable override in $GITWEB_CONFIG
- # $feature{'patches'}{'override'} = 1;
- # and in project config gitweb.patches = 0|n;
- # where n is the maximum number of patches allowed in a patchset.
- 'patches' => {
- 'sub' => \&feature_patches,
- 'override' => 0,
- 'default' => [16]},
-
- # Avatar support. When this feature is enabled, views such as
- # shortlog or commit will display an avatar associated with
- # the email of the committer(s) and/or author(s).
-
- # Currently available providers are gravatar and picon.
- # If an unknown provider is specified, the feature is disabled.
-
- # Gravatar depends on Digest::MD5.
- # Picon currently relies on the indiana.edu database.
-
- # To enable system wide have in $GITWEB_CONFIG
- # $feature{'avatar'}{'default'} = [''];
- # where is either gravatar or picon.
- # To have project specific config enable override in $GITWEB_CONFIG
- # $feature{'avatar'}{'override'} = 1;
- # and in project config gitweb.avatar = ;
- 'avatar' => {
- 'sub' => \&feature_avatar,
- 'override' => 0,
- 'default' => ['']},
-
- # Enable displaying how much time and how many git commands
- # it took to generate and display page. Disabled by default.
- # Project specific override is not supported.
- 'timed' => {
- 'override' => 0,
- 'default' => [0]},
-
- # Enable turning some links into links to actions which require
- # JavaScript to run (like 'blame_incremental'). Not enabled by
- # default. Project specific override is currently not supported.
- 'javascript-actions' => {
- 'override' => 0,
- 'default' => [0]},
-
- # Enable and configure ability to change common timezone for dates
- # in gitweb output via JavaScript. Enabled by default.
- # Project specific override is not supported.
- 'javascript-timezone' => {
- 'override' => 0,
- 'default' => [
- 'local', # default timezone: 'utc', 'local', or '(-|+)HHMM' format,
- # or undef to turn off this feature
- 'gitweb_tz', # name of cookie where to store selected timezone
- 'datetime', # CSS class used to mark up dates for manipulation
- ]},
-
- # Syntax highlighting support. This is based on Daniel Svensson's
- # and Sham Chukoury's work in gitweb-xmms2.git.
- # It requires the 'highlight' program present in $PATH,
- # and therefore is disabled by default.
-
- # To enable system wide have in $GITWEB_CONFIG
- # $feature{'highlight'}{'default'} = [1];
-
- 'highlight' => {
- 'sub' => sub { feature_bool('highlight', @_) },
- 'override' => 0,
- 'default' => [0]},
-
- # Enable displaying of remote heads in the heads list
-
- # To enable system wide have in $GITWEB_CONFIG
- # $feature{'remote_heads'}{'default'} = [1];
- # To have project specific config enable override in $GITWEB_CONFIG
- # $feature{'remote_heads'}{'override'} = 1;
- # and in project config gitweb.remoteheads = 0|1;
- 'remote_heads' => {
- 'sub' => sub { feature_bool('remote_heads', @_) },
- 'override' => 0,
- 'default' => [0]},
-
- # Enable showing branches under other refs in addition to heads
-
- # To set system wide extra branch refs have in $GITWEB_CONFIG
- # $feature{'extra-branch-refs'}{'default'} = ['dirs', 'of', 'choice'];
- # To have project specific config enable override in $GITWEB_CONFIG
- # $feature{'extra-branch-refs'}{'override'} = 1;
- # and in project config gitweb.extrabranchrefs = dirs of choice
- # Every directory is separated with whitespace.
-
- 'extra-branch-refs' => {
- 'sub' => \&feature_extra_branch_refs,
- 'override' => 0,
- 'default' => []},
-);
-
-sub gitweb_get_feature {
- my ($name) = @_;
- return unless exists $feature{$name};
- my ($sub, $override, @defaults) = (
- $feature{$name}{'sub'},
- $feature{$name}{'override'},
- @{$feature{$name}{'default'}});
- # project specific override is possible only if we have project
- our $git_dir; # global variable, declared later
- if (!$override || !defined $git_dir) {
- return @defaults;
- }
- if (!defined $sub) {
- warn "feature $name is not overridable";
- return @defaults;
- }
- return $sub->(@defaults);
-}
-
-# A wrapper to check if a given feature is enabled.
-# With this, you can say
-#
-# my $bool_feat = gitweb_check_feature('bool_feat');
-# gitweb_check_feature('bool_feat') or somecode;
-#
-# instead of
-#
-# my ($bool_feat) = gitweb_get_feature('bool_feat');
-# (gitweb_get_feature('bool_feat'))[0] or somecode;
-#
-sub gitweb_check_feature {
- return (gitweb_get_feature(@_))[0];
-}
-
-
-sub feature_bool {
- my $key = shift;
- my ($val) = git_get_project_config($key, '--bool');
-
- if (!defined $val) {
- return ($_[0]);
- } elsif ($val eq 'true') {
- return (1);
- } elsif ($val eq 'false') {
- return (0);
- }
-}
-
-sub feature_snapshot {
- my (@fmts) = @_;
-
- my ($val) = git_get_project_config('snapshot');
-
- if ($val) {
- @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
- }
-
- return @fmts;
-}
-
-sub feature_patches {
- my @val = (git_get_project_config('patches', '--int'));
-
- if (@val) {
- return @val;
- }
-
- return ($_[0]);
-}
-
-sub feature_avatar {
- my @val = (git_get_project_config('avatar'));
-
- return @val ? @val : @_;
-}
-
-sub feature_extra_branch_refs {
- my (@branch_refs) = @_;
- my $values = git_get_project_config('extrabranchrefs');
-
- if ($values) {
- $values = config_to_multi ($values);
- @branch_refs = ();
- foreach my $value (@{$values}) {
- push @branch_refs, split /\s+/, $value;
- }
- }
-
- return @branch_refs;
-}
-
-# checking HEAD file with -e is fragile if the repository was
-# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
-# and then pruned.
-sub check_head_link {
- my ($dir) = @_;
- my $headfile = "$dir/HEAD";
- return ((-e $headfile) ||
- (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
-}
-
-sub check_export_ok {
- my ($dir) = @_;
- return (check_head_link($dir) &&
- (!$export_ok || -e "$dir/$export_ok") &&
- (!$export_auth_hook || $export_auth_hook->($dir)));
-}
-
-# process alternate names for backward compatibility
-# filter out unsupported (unknown) snapshot formats
-sub filter_snapshot_fmts {
- my @fmts = @_;
-
- @fmts = map {
- exists $known_snapshot_format_aliases{$_} ?
- $known_snapshot_format_aliases{$_} : $_} @fmts;
- @fmts = grep {
- exists $known_snapshot_formats{$_} &&
- !$known_snapshot_formats{$_}{'disabled'}} @fmts;
-}
-
-sub filter_and_validate_refs {
- my @refs = @_;
- my %unique_refs = ();
-
- foreach my $ref (@refs) {
- die_error(500, "Invalid ref '$ref' in 'extra-branch-refs' feature") unless (is_valid_ref_format($ref));
- # 'heads' are added implicitly in get_branch_refs().
- $unique_refs{$ref} = 1 if ($ref ne 'heads');
- }
- return sort keys %unique_refs;
-}
-
-# If it is set to code reference, it is code that it is to be run once per
-# request, allowing updating configurations that change with each request,
-# while running other code in config file only once.
-#
-# Otherwise, if it is false then gitweb would process config file only once;
-# if it is true then gitweb config would be run for each request.
-our $per_request_config = 1;
-
-# read and parse gitweb config file given by its parameter.
-# returns true on success, false on recoverable error, allowing
-# to chain this subroutine, using first file that exists.
-# dies on errors during parsing config file, as it is unrecoverable.
-sub read_config_file {
- my $filename = shift;
- return unless defined $filename;
- # die if there are errors parsing config file
- if (-e $filename) {
- do $filename;
- die $@ if $@;
- return 1;
- }
- return;
-}
-
-our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON);
-sub evaluate_gitweb_config {
- our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "gitweb_config.perl";
- our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "/etc/gitweb.conf";
- our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "/etc/gitweb-common.conf";
-
- # Protect against duplications of file names, to not read config twice.
- # Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so
- # there possibility of duplication of filename there doesn't matter.
- $GITWEB_CONFIG = "" if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON);
- $GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
-
- # Common system-wide settings for convenience.
- # Those settings can be ovverriden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
- read_config_file($GITWEB_CONFIG_COMMON);
-
- # Use first config file that exists. This means use the per-instance
- # GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG.
- read_config_file($GITWEB_CONFIG) and return;
- read_config_file($GITWEB_CONFIG_SYSTEM);
-}
-
-# Get loadavg of system, to compare against $maxload.
-# Currently it requires '/proc/loadavg' present to get loadavg;
-# if it is not present it returns 0, which means no load checking.
-sub get_loadavg {
- if( -e '/proc/loadavg' ){
- open my $fd, '<', '/proc/loadavg'
- or return 0;
- my @load = split(/\s+/, scalar <$fd>);
- close $fd;
-
- # The first three columns measure CPU and IO utilization of the last one,
- # five, and 10 minute periods. The fourth column shows the number of
- # currently running processes and the total number of processes in the m/n
- # format. The last column displays the last process ID used.
- return $load[0] || 0;
- }
- # additional checks for load average should go here for things that don't export
- # /proc/loadavg
-
- return 0;
-}
-
-# version of the core git binary
-our $git_version;
-sub evaluate_git_version {
- our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
- $number_of_git_cmds++;
-}
-
-sub check_loadavg {
- if (defined $maxload && get_loadavg() > $maxload) {
- die_error(503, "The load average on the server is too high");
- }
-}
-
-# ======================================================================
-# input validation and dispatch
-
-# input parameters can be collected from a variety of sources (presently, CGI
-# and PATH_INFO), so we define an %input_params hash that collects them all
-# together during validation: this allows subsequent uses (e.g. href()) to be
-# agnostic of the parameter origin
-
-our %input_params = ();
-
-# input parameters are stored with the long parameter name as key. This will
-# also be used in the href subroutine to convert parameters to their CGI
-# equivalent, and since the href() usage is the most frequent one, we store
-# the name -> CGI key mapping here, instead of the reverse.
-#
-# XXX: Warning: If you touch this, check the search form for updating,
-# too.
-
-our @cgi_param_mapping = (
- project => "p",
- action => "a",
- file_name => "f",
- file_parent => "fp",
- hash => "h",
- hash_parent => "hp",
- hash_base => "hb",
- hash_parent_base => "hpb",
- page => "pg",
- order => "o",
- searchtext => "s",
- searchtype => "st",
- snapshot_format => "sf",
- extra_options => "opt",
- search_use_regexp => "sr",
- ctag => "by_tag",
- diff_style => "ds",
- project_filter => "pf",
- # this must be last entry (for manipulation from JavaScript)
- javascript => "js"
-);
-our %cgi_param_mapping = @cgi_param_mapping;
-
-# we will also need to know the possible actions, for validation
-our %actions = (
- "blame" => \&git_blame,
- "blame_incremental" => \&git_blame_incremental,
- "blame_data" => \&git_blame_data,
- "blobdiff" => \&git_blobdiff,
- "blobdiff_plain" => \&git_blobdiff_plain,
- "blob" => \&git_blob,
- "blob_plain" => \&git_blob_plain,
- "commitdiff" => \&git_commitdiff,
- "commitdiff_plain" => \&git_commitdiff_plain,
- "commit" => \&git_commit,
- "forks" => \&git_forks,
- "heads" => \&git_heads,
- "history" => \&git_history,
- "log" => \&git_log,
- "patch" => \&git_patch,
- "patches" => \&git_patches,
- "remotes" => \&git_remotes,
- "rss" => \&git_rss,
- "atom" => \&git_atom,
- "search" => \&git_search,
- "search_help" => \&git_search_help,
- "shortlog" => \&git_shortlog,
- "summary" => \&git_summary,
- "tag" => \&git_tag,
- "tags" => \&git_tags,
- "tree" => \&git_tree,
- "snapshot" => \&git_snapshot,
- "object" => \&git_object,
- # those below don't need $project
- "opml" => \&git_opml,
- "project_list" => \&git_project_list,
- "project_index" => \&git_project_index,
-);
-
-# finally, we have the hash of allowed extra_options for the commands that
-# allow them
-our %allowed_options = (
- "--no-merges" => [ qw(rss atom log shortlog history) ],
-);
-
-# fill %input_params with the CGI parameters. All values except for 'opt'
-# should be single values, but opt can be an array. We should probably
-# build an array of parameters that can be multi-valued, but since for the time
-# being it's only this one, we just single it out
-sub evaluate_query_params {
- our $cgi;
-
- while (my ($name, $symbol) = each %cgi_param_mapping) {
- if ($symbol eq 'opt') {
- $input_params{$name} = [ map { decode_utf8($_) } $cgi->multi_param($symbol) ];
- } else {
- $input_params{$name} = decode_utf8($cgi->param($symbol));
- }
- }
-}
-
-# now read PATH_INFO and update the parameter list for missing parameters
-sub evaluate_path_info {
- return if defined $input_params{'project'};
- return if !$path_info;
- $path_info =~ s,^/+,,;
- return if !$path_info;
-
- # find which part of PATH_INFO is project
- my $project = $path_info;
- $project =~ s,/+$,,;
- while ($project && !check_head_link("$projectroot/$project")) {
- $project =~ s,/*[^/]*$,,;
- }
- return unless $project;
- $input_params{'project'} = $project;
-
- # do not change any parameters if an action is given using the query string
- return if $input_params{'action'};
- $path_info =~ s,^\Q$project\E/*,,;
-
- # next, check if we have an action
- my $action = $path_info;
- $action =~ s,/.*$,,;
- if (exists $actions{$action}) {
- $path_info =~ s,^$action/*,,;
- $input_params{'action'} = $action;
- }
-
- # list of actions that want hash_base instead of hash, but can have no
- # pathname (f) parameter
- my @wants_base = (
- 'tree',
- 'history',
- );
-
- # we want to catch, among others
- # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
- my ($parentrefname, $parentpathname, $refname, $pathname) =
- ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
-
- # first, analyze the 'current' part
- if (defined $pathname) {
- # we got "branch:filename" or "branch:dir/"
- # we could use git_get_type(branch:pathname), but:
- # - it needs $git_dir
- # - it does a git() call
- # - the convention of terminating directories with a slash
- # makes it superfluous
- # - embedding the action in the PATH_INFO would make it even
- # more superfluous
- $pathname =~ s,^/+,,;
- if (!$pathname || substr($pathname, -1) eq "/") {
- $input_params{'action'} ||= "tree";
- $pathname =~ s,/$,,;
- } else {
- # the default action depends on whether we had parent info
- # or not
- if ($parentrefname) {
- $input_params{'action'} ||= "blobdiff_plain";
- } else {
- $input_params{'action'} ||= "blob_plain";
- }
- }
- $input_params{'hash_base'} ||= $refname;
- $input_params{'file_name'} ||= $pathname;
- } elsif (defined $refname) {
- # we got "branch". In this case we have to choose if we have to
- # set hash or hash_base.
- #
- # Most of the actions without a pathname only want hash to be
- # set, except for the ones specified in @wants_base that want
- # hash_base instead. It should also be noted that hand-crafted
- # links having 'history' as an action and no pathname or hash
- # set will fail, but that happens regardless of PATH_INFO.
- if (defined $parentrefname) {
- # if there is parent let the default be 'shortlog' action
- # (for http://git.example.com/repo.git/A..B links); if there
- # is no parent, dispatch will detect type of object and set
- # action appropriately if required (if action is not set)
- $input_params{'action'} ||= "shortlog";
- }
- if ($input_params{'action'} &&
- grep { $_ eq $input_params{'action'} } @wants_base) {
- $input_params{'hash_base'} ||= $refname;
- } else {
- $input_params{'hash'} ||= $refname;
- }
- }
-
- # next, handle the 'parent' part, if present
- if (defined $parentrefname) {
- # a missing pathspec defaults to the 'current' filename, allowing e.g.
- # someproject/blobdiff/oldrev..newrev:/filename
- if ($parentpathname) {
- $parentpathname =~ s,^/+,,;
- $parentpathname =~ s,/$,,;
- $input_params{'file_parent'} ||= $parentpathname;
- } else {
- $input_params{'file_parent'} ||= $input_params{'file_name'};
- }
- # we assume that hash_parent_base is wanted if a path was specified,
- # or if the action wants hash_base instead of hash
- if (defined $input_params{'file_parent'} ||
- grep { $_ eq $input_params{'action'} } @wants_base) {
- $input_params{'hash_parent_base'} ||= $parentrefname;
- } else {
- $input_params{'hash_parent'} ||= $parentrefname;
- }
- }
-
- # for the snapshot action, we allow URLs in the form
- # $project/snapshot/$hash.ext
- # where .ext determines the snapshot and gets removed from the
- # passed $refname to provide the $hash.
- #
- # To be able to tell that $refname includes the format extension, we
- # require the following two conditions to be satisfied:
- # - the hash input parameter MUST have been set from the $refname part
- # of the URL (i.e. they must be equal)
- # - the snapshot format MUST NOT have been defined already (e.g. from
- # CGI parameter sf)
- # It's also useless to try any matching unless $refname has a dot,
- # so we check for that too
- if (defined $input_params{'action'} &&
- $input_params{'action'} eq 'snapshot' &&
- defined $refname && index($refname, '.') != -1 &&
- $refname eq $input_params{'hash'} &&
- !defined $input_params{'snapshot_format'}) {
- # We loop over the known snapshot formats, checking for
- # extensions. Allowed extensions are both the defined suffix
- # (which includes the initial dot already) and the snapshot
- # format key itself, with a prepended dot
- while (my ($fmt, $opt) = each %known_snapshot_formats) {
- my $hash = $refname;
- unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
- next;
- }
- my $sfx = $1;
- # a valid suffix was found, so set the snapshot format
- # and reset the hash parameter
- $input_params{'snapshot_format'} = $fmt;
- $input_params{'hash'} = $hash;
- # we also set the format suffix to the one requested
- # in the URL: this way a request for e.g. .tgz returns
- # a .tgz instead of a .tar.gz
- $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
- last;
- }
- }
-}
-
-our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
- $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
- $searchtext, $search_regexp, $project_filter);
-sub evaluate_and_validate_params {
- our $action = $input_params{'action'};
- if (defined $action) {
- if (!is_valid_action($action)) {
- die_error(400, "Invalid action parameter");
- }
- }
-
- # parameters which are pathnames
- our $project = $input_params{'project'};
- if (defined $project) {
- if (!is_valid_project($project)) {
- undef $project;
- die_error(404, "No such project");
- }
- }
-
- our $project_filter = $input_params{'project_filter'};
- if (defined $project_filter) {
- if (!is_valid_pathname($project_filter)) {
- die_error(404, "Invalid project_filter parameter");
- }
- }
-
- our $file_name = $input_params{'file_name'};
- if (defined $file_name) {
- if (!is_valid_pathname($file_name)) {
- die_error(400, "Invalid file parameter");
- }
- }
-
- our $file_parent = $input_params{'file_parent'};
- if (defined $file_parent) {
- if (!is_valid_pathname($file_parent)) {
- die_error(400, "Invalid file parent parameter");
- }
- }
-
- # parameters which are refnames
- our $hash = $input_params{'hash'};
- if (defined $hash) {
- if (!is_valid_refname($hash)) {
- die_error(400, "Invalid hash parameter");
- }
- }
-
- our $hash_parent = $input_params{'hash_parent'};
- if (defined $hash_parent) {
- if (!is_valid_refname($hash_parent)) {
- die_error(400, "Invalid hash parent parameter");
- }
- }
-
- our $hash_base = $input_params{'hash_base'};
- if (defined $hash_base) {
- if (!is_valid_refname($hash_base)) {
- die_error(400, "Invalid hash base parameter");
- }
- }
-
- our @extra_options = @{$input_params{'extra_options'}};
- # @extra_options is always defined, since it can only be (currently) set from
- # CGI, and $cgi->param() returns the empty array in array context if the param
- # is not set
- foreach my $opt (@extra_options) {
- if (not exists $allowed_options{$opt}) {
- die_error(400, "Invalid option parameter");
- }
- if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
- die_error(400, "Invalid option parameter for this action");
- }
- }
-
- our $hash_parent_base = $input_params{'hash_parent_base'};
- if (defined $hash_parent_base) {
- if (!is_valid_refname($hash_parent_base)) {
- die_error(400, "Invalid hash parent base parameter");
- }
- }
-
- # other parameters
- our $page = $input_params{'page'};
- if (defined $page) {
- if ($page =~ m/[^0-9]/) {
- die_error(400, "Invalid page parameter");
- }
- }
-
- our $searchtype = $input_params{'searchtype'};
- if (defined $searchtype) {
- if ($searchtype =~ m/[^a-z]/) {
- die_error(400, "Invalid searchtype parameter");
- }
- }
-
- our $search_use_regexp = $input_params{'search_use_regexp'};
-
- our $searchtext = $input_params{'searchtext'};
- our $search_regexp = undef;
- if (defined $searchtext) {
- if (length($searchtext) < 2) {
- die_error(403, "At least two characters are required for search parameter");
- }
- if ($search_use_regexp) {
- $search_regexp = $searchtext;
- if (!eval { qr/$search_regexp/; 1; }) {
- (my $error = $@) =~ s/ at \S+ line \d+.*\n?//;
- die_error(400, "Invalid search regexp '$search_regexp'",
- esc_html($error));
- }
- } else {
- $search_regexp = quotemeta $searchtext;
- }
- }
-}
-
-# path to the current git repository
-our $git_dir;
-sub evaluate_git_dir {
- our $git_dir = "$projectroot/$project" if $project;
-}
-
-our (@snapshot_fmts, $git_avatar, @extra_branch_refs);
-sub configure_gitweb_features {
- # list of supported snapshot formats
- our @snapshot_fmts = gitweb_get_feature('snapshot');
- @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
-
- # check that the avatar feature is set to a known provider name,
- # and for each provider check if the dependencies are satisfied.
- # if the provider name is invalid or the dependencies are not met,
- # reset $git_avatar to the empty string.
- our ($git_avatar) = gitweb_get_feature('avatar');
- if ($git_avatar eq 'gravatar') {
- $git_avatar = '' unless (eval { require Digest::MD5; 1; });
- } elsif ($git_avatar eq 'picon') {
- # no dependencies
- } else {
- $git_avatar = '';
- }
-
- our @extra_branch_refs = gitweb_get_feature('extra-branch-refs');
- @extra_branch_refs = filter_and_validate_refs (@extra_branch_refs);
-}
-
-sub get_branch_refs {
- return ('heads', @extra_branch_refs);
-}
-
-# custom error handler: 'die ' is Internal Server Error
-sub handle_errors_html {
- my $msg = shift; # it is already HTML escaped
-
- # to avoid infinite loop where error occurs in die_error,
- # change handler to default handler, disabling handle_errors_html
- set_message("Error occurred when inside die_error:\n$msg");
-
- # you cannot jump out of die_error when called as error handler;
- # the subroutine set via CGI::Carp::set_message is called _after_
- # HTTP headers are already written, so it cannot write them itself
- die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
-}
-set_message(\&handle_errors_html);
-
-# dispatch
-sub dispatch {
- if (!defined $action) {
- if (defined $hash) {
- $action = git_get_type($hash);
- $action or die_error(404, "Object does not exist");
- } elsif (defined $hash_base && defined $file_name) {
- $action = git_get_type("$hash_base:$file_name");
- $action or die_error(404, "File or directory does not exist");
- } elsif (defined $project) {
- $action = 'summary';
- } else {
- $action = 'project_list';
- }
- }
- if (!defined($actions{$action})) {
- die_error(400, "Unknown action");
- }
- if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
- !$project) {
- die_error(400, "Project needed");
- }
- $actions{$action}->();
-}
-
-sub reset_timer {
- our $t0 = [ gettimeofday() ]
- if defined $t0;
- our $number_of_git_cmds = 0;
-}
-
-our $first_request = 1;
-sub run_request {
- reset_timer();
-
- evaluate_uri();
- if ($first_request) {
- evaluate_gitweb_config();
- evaluate_git_version();
- }
- if ($per_request_config) {
- if (ref($per_request_config) eq 'CODE') {
- $per_request_config->();
- } elsif (!$first_request) {
- evaluate_gitweb_config();
- }
- }
- check_loadavg();
-
- # $projectroot and $projects_list might be set in gitweb config file
- $projects_list ||= $projectroot;
-
- evaluate_query_params();
- evaluate_path_info();
- evaluate_and_validate_params();
- evaluate_git_dir();
-
- configure_gitweb_features();
-
- dispatch();
-}
-
-our $is_last_request = sub { 1 };
-our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
-our $CGI = 'CGI';
-our $cgi;
-sub configure_as_fcgi {
- require CGI::Fast;
- our $CGI = 'CGI::Fast';
-
- my $request_number = 0;
- # let each child service 100 requests
- our $is_last_request = sub { ++$request_number > 100 };
-}
-sub evaluate_argv {
- my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
- configure_as_fcgi()
- if $script_name =~ /\.fcgi$/;
-
- return unless (@ARGV);
-
- require Getopt::Long;
- Getopt::Long::GetOptions(
- 'fastcgi|fcgi|f' => \&configure_as_fcgi,
- 'nproc|n=i' => sub {
- my ($arg, $val) = @_;
- return unless eval { require FCGI::ProcManager; 1; };
- my $proc_manager = FCGI::ProcManager->new({
- n_processes => $val,
- });
- our $pre_listen_hook = sub { $proc_manager->pm_manage() };
- our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
- our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
- },
- );
-}
-
-sub run {
- evaluate_argv();
-
- $first_request = 1;
- $pre_listen_hook->()
- if $pre_listen_hook;
-
- REQUEST:
- while ($cgi = $CGI->new()) {
- $pre_dispatch_hook->()
- if $pre_dispatch_hook;
-
- run_request();
-
- $post_dispatch_hook->()
- if $post_dispatch_hook;
- $first_request = 0;
-
- last REQUEST if ($is_last_request->());
- }
-
- DONE_GITWEB:
- 1;
-}
-
-run();
-
-if (defined caller) {
- # wrapped in a subroutine processing requests,
- # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
- return;
-} else {
- # pure CGI script, serving single request
- exit;
-}
-
-## ======================================================================
-## action links
-
-# possible values of extra options
-# -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
-# -replay => 1 - start from a current view (replay with modifications)
-# -path_info => 0|1 - don't use/use path_info URL (if possible)
-# -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
-sub href {
- my %params = @_;
- # default is to use -absolute url() i.e. $my_uri
- my $href = $params{-full} ? $my_url : $my_uri;
-
- # implicit -replay, must be first of implicit params
- $params{-replay} = 1 if (keys %params == 1 && $params{-anchor});
-
- $params{'project'} = $project unless exists $params{'project'};
-
- if ($params{-replay}) {
- while (my ($name, $symbol) = each %cgi_param_mapping) {
- if (!exists $params{$name}) {
- $params{$name} = $input_params{$name};
- }
- }
- }
-
- my $use_pathinfo = gitweb_check_feature('pathinfo');
- if (defined $params{'project'} &&
- (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
- # try to put as many parameters as possible in PATH_INFO:
- # - project name
- # - action
- # - hash_parent or hash_parent_base:/file_parent
- # - hash or hash_base:/filename
- # - the snapshot_format as an appropriate suffix
-
- # When the script is the root DirectoryIndex for the domain,
- # $href here would be something like http://gitweb.example.com/
- # Thus, we strip any trailing / from $href, to spare us double
- # slashes in the final URL
- $href =~ s,/$,,;
-
- # Then add the project name, if present
- $href .= "/".esc_path_info($params{'project'});
- delete $params{'project'};
-
- # since we destructively absorb parameters, we keep this
- # boolean that remembers if we're handling a snapshot
- my $is_snapshot = $params{'action'} eq 'snapshot';
-
- # Summary just uses the project path URL, any other action is
- # added to the URL
- if (defined $params{'action'}) {
- $href .= "/".esc_path_info($params{'action'})
- unless $params{'action'} eq 'summary';
- delete $params{'action'};
- }
-
- # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
- # stripping nonexistent or useless pieces
- $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
- || $params{'hash_parent'} || $params{'hash'});
- if (defined $params{'hash_base'}) {
- if (defined $params{'hash_parent_base'}) {
- $href .= esc_path_info($params{'hash_parent_base'});
- # skip the file_parent if it's the same as the file_name
- if (defined $params{'file_parent'}) {
- if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
- delete $params{'file_parent'};
- } elsif ($params{'file_parent'} !~ /\.\./) {
- $href .= ":/".esc_path_info($params{'file_parent'});
- delete $params{'file_parent'};
- }
- }
- $href .= "..";
- delete $params{'hash_parent'};
- delete $params{'hash_parent_base'};
- } elsif (defined $params{'hash_parent'}) {
- $href .= esc_path_info($params{'hash_parent'}). "..";
- delete $params{'hash_parent'};
- }
-
- $href .= esc_path_info($params{'hash_base'});
- if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
- $href .= ":/".esc_path_info($params{'file_name'});
- delete $params{'file_name'};
- }
- delete $params{'hash'};
- delete $params{'hash_base'};
- } elsif (defined $params{'hash'}) {
- $href .= esc_path_info($params{'hash'});
- delete $params{'hash'};
- }
-
- # If the action was a snapshot, we can absorb the
- # snapshot_format parameter too
- if ($is_snapshot) {
- my $fmt = $params{'snapshot_format'};
- # snapshot_format should always be defined when href()
- # is called, but just in case some code forgets, we
- # fall back to the default
- $fmt ||= $snapshot_fmts[0];
- $href .= $known_snapshot_formats{$fmt}{'suffix'};
- delete $params{'snapshot_format'};
- }
- }
-
- # now encode the parameters explicitly
- my @result = ();
- for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
- my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
- if (defined $params{$name}) {
- if (ref($params{$name}) eq "ARRAY") {
- foreach my $par (@{$params{$name}}) {
- push @result, $symbol . "=" . esc_param($par);
- }
- } else {
- push @result, $symbol . "=" . esc_param($params{$name});
- }
- }
- }
- $href .= "?" . join(';', @result) if scalar @result;
-
- # final transformation: trailing spaces must be escaped (URI-encoded)
- $href =~ s/(\s+)$/CGI::escape($1)/e;
-
- if ($params{-anchor}) {
- $href .= "#".esc_param($params{-anchor});
- }
-
- return $href;
-}
-
-
-## ======================================================================
-## validation, quoting/unquoting and escaping
-
-sub is_valid_action {
- my $input = shift;
- return undef unless exists $actions{$input};
- return 1;
-}
-
-sub is_valid_project {
- my $input = shift;
-
- return unless defined $input;
- if (!is_valid_pathname($input) ||
- !(-d "$projectroot/$input") ||
- !check_export_ok("$projectroot/$input") ||
- ($strict_export && !project_in_list($input))) {
- return undef;
- } else {
- return 1;
- }
-}
-
-sub is_valid_pathname {
- my $input = shift;
-
- return undef unless defined $input;
- # no '.' or '..' as elements of path, i.e. no '.' or '..'
- # at the beginning, at the end, and between slashes.
- # also this catches doubled slashes
- if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
- return undef;
- }
- # no null characters
- if ($input =~ m!\0!) {
- return undef;
- }
- return 1;
-}
-
-sub is_valid_ref_format {
- my $input = shift;
-
- return undef unless defined $input;
- # restrictions on ref name according to git-check-ref-format
- if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
- return undef;
- }
- return 1;
-}
-
-sub is_valid_refname {
- my $input = shift;
-
- return undef unless defined $input;
- # textual hashes are O.K.
- if ($input =~ m/^[0-9a-fA-F]{40}$/) {
- return 1;
- }
- # it must be correct pathname
- is_valid_pathname($input) or return undef;
- # check git-check-ref-format restrictions
- is_valid_ref_format($input) or return undef;
- return 1;
-}
-
-# decode sequences of octets in utf8 into Perl's internal form,
-# which is utf-8 with utf8 flag set if needed. gitweb writes out
-# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
-sub to_utf8 {
- my $str = shift;
- return undef unless defined $str;
-
- if (utf8::is_utf8($str) || utf8::decode($str)) {
- return $str;
- } else {
- return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
- }
-}
-
-# quote unsafe chars, but keep the slash, even when it's not
-# correct, but quoted slashes look too horrible in bookmarks
-sub esc_param {
- my $str = shift;
- return undef unless defined $str;
- $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
- $str =~ s/ /\+/g;
- return $str;
-}
-
-# the quoting rules for path_info fragment are slightly different
-sub esc_path_info {
- my $str = shift;
- return undef unless defined $str;
-
- # path_info doesn't treat '+' as space (specially), but '?' must be escaped
- $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg;
-
- return $str;
-}
-
-# quote unsafe chars in whole URL, so some characters cannot be quoted
-sub esc_url {
- my $str = shift;
- return undef unless defined $str;
- $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
- $str =~ s/ /\+/g;
- return $str;
-}
-
-# quote unsafe characters in HTML attributes
-sub esc_attr {
-
- # for XHTML conformance escaping '"' to '"' is not enough
- return esc_html(@_);
-}
-
-# replace invalid utf8 character with SUBSTITUTION sequence
-sub esc_html {
- my $str = shift;
- my %opts = @_;
-
- return undef unless defined $str;
-
- $str = to_utf8($str);
- $str = $cgi->escapeHTML($str);
- if ($opts{'-nbsp'}) {
- $str =~ s/ / /g;
- }
- $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
- return $str;
-}
-
-# quote control characters and escape filename to HTML
-sub esc_path {
- my $str = shift;
- my %opts = @_;
-
- return undef unless defined $str;
-
- $str = to_utf8($str);
- $str = $cgi->escapeHTML($str);
- if ($opts{'-nbsp'}) {
- $str =~ s/ / /g;
- }
- $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
- return $str;
-}
-
-# Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0)
-sub sanitize {
- my $str = shift;
-
- return undef unless defined $str;
-
- $str = to_utf8($str);
- $str =~ s|([[:cntrl:]])|(index("\t\n\r", $1) != -1 ? $1 : quot_cec($1))|eg;
- return $str;
-}
-
-# Make control characters "printable", using character escape codes (CEC)
-sub quot_cec {
- my $cntrl = shift;
- my %opts = @_;
- my %es = ( # character escape codes, aka escape sequences
- "\t" => '\t', # tab (HT)
- "\n" => '\n', # line feed (LF)
- "\r" => '\r', # carrige return (CR)
- "\f" => '\f', # form feed (FF)
- "\b" => '\b', # backspace (BS)
- "\a" => '\a', # alarm (bell) (BEL)
- "\e" => '\e', # escape (ESC)
- "\013" => '\v', # vertical tab (VT)
- "\000" => '\0', # nul character (NUL)
- );
- my $chr = ( (exists $es{$cntrl})
- ? $es{$cntrl}
- : sprintf('\%2x', ord($cntrl)) );
- if ($opts{-nohtml}) {
- return $chr;
- } else {
- return "$chr";
- }
-}
-
-# Alternatively use unicode control pictures codepoints,
-# Unicode "printable representation" (PR)
-sub quot_upr {
- my $cntrl = shift;
- my %opts = @_;
-
- my $chr = sprintf('%04d;', 0x2400+ord($cntrl));
- if ($opts{-nohtml}) {
- return $chr;
- } else {
- return "$chr";
- }
-}
-
-# git may return quoted and escaped filenames
-sub unquote {
- my $str = shift;
-
- sub unq {
- my $seq = shift;
- my %es = ( # character escape codes, aka escape sequences
- 't' => "\t", # tab (HT, TAB)
- 'n' => "\n", # newline (NL)
- 'r' => "\r", # return (CR)
- 'f' => "\f", # form feed (FF)
- 'b' => "\b", # backspace (BS)
- 'a' => "\a", # alarm (bell) (BEL)
- 'e' => "\e", # escape (ESC)
- 'v' => "\013", # vertical tab (VT)
- );
-
- if ($seq =~ m/^[0-7]{1,3}$/) {
- # octal char sequence
- return chr(oct($seq));
- } elsif (exists $es{$seq}) {
- # C escape sequence, aka character escape code
- return $es{$seq};
- }
- # quoted ordinary character
- return $seq;
- }
-
- if ($str =~ m/^"(.*)"$/) {
- # needs unquoting
- $str = $1;
- $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
- }
- return $str;
-}
-
-# escape tabs (convert tabs to spaces)
-sub untabify {
- my $line = shift;
-
- while ((my $pos = index($line, "\t")) != -1) {
- if (my $count = (8 - ($pos % 8))) {
- my $spaces = ' ' x $count;
- $line =~ s/\t/$spaces/;
- }
- }
-
- return $line;
-}
-
-sub project_in_list {
- my $project = shift;
- my @list = git_get_projects_list();
- return @list && scalar(grep { $_->{'path'} eq $project } @list);
-}
-
-## ----------------------------------------------------------------------
-## HTML aware string manipulation
-
-# Try to chop given string on a word boundary between position
-# $len and $len+$add_len. If there is no word boundary there,
-# chop at $len+$add_len. Do not chop if chopped part plus ellipsis
-# (marking chopped part) would be longer than given string.
-sub chop_str {
- my $str = shift;
- my $len = shift;
- my $add_len = shift || 10;
- my $where = shift || 'right'; # 'left' | 'center' | 'right'
-
- # Make sure perl knows it is utf8 encoded so we don't
- # cut in the middle of a utf8 multibyte char.
- $str = to_utf8($str);
-
- # allow only $len chars, but don't cut a word if it would fit in $add_len
- # if it doesn't fit, cut it if it's still longer than the dots we would add
- # remove chopped character entities entirely
-
- # when chopping in the middle, distribute $len into left and right part
- # return early if chopping wouldn't make string shorter
- if ($where eq 'center') {
- return $str if ($len + 5 >= length($str)); # filler is length 5
- $len = int($len/2);
- } else {
- return $str if ($len + 4 >= length($str)); # filler is length 4
- }
-
- # regexps: ending and beginning with word part up to $add_len
- my $endre = qr/.{$len}\w{0,$add_len}/;
- my $begre = qr/\w{0,$add_len}.{$len}/;
-
- if ($where eq 'left') {
- $str =~ m/^(.*?)($begre)$/;
- my ($lead, $body) = ($1, $2);
- if (length($lead) > 4) {
- $lead = " ...";
- }
- return "$lead$body";
-
- } elsif ($where eq 'center') {
- $str =~ m/^($endre)(.*)$/;
- my ($left, $str) = ($1, $2);
- $str =~ m/^(.*?)($begre)$/;
- my ($mid, $right) = ($1, $2);
- if (length($mid) > 5) {
- $mid = " ... ";
- }
- return "$left$mid$right";
-
- } else {
- $str =~ m/^($endre)(.*)$/;
- my $body = $1;
- my $tail = $2;
- if (length($tail) > 4) {
- $tail = "... ";
- }
- return "$body$tail";
- }
-}
-
-# takes the same arguments as chop_str, but also wraps a around the
-# result with a title attribute if it does get chopped. Additionally, the
-# string is HTML-escaped.
-sub chop_and_escape_str {
- my ($str) = @_;
-
- my $chopped = chop_str(@_);
- $str = to_utf8($str);
- if ($chopped eq $str) {
- return esc_html($chopped);
- } else {
- $str =~ s/[[:cntrl:]]/?/g;
- return $cgi->span({-title=>$str}, esc_html($chopped));
- }
-}
-
-# Highlight selected fragments of string, using given CSS class,
-# and escape HTML. It is assumed that fragments do not overlap.
-# Regions are passed as list of pairs (array references).
-#
-# Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
-# 'foobar'
-sub esc_html_hl_regions {
- my ($str, $css_class, @sel) = @_;
- my %opts = grep { ref($_) ne 'ARRAY' } @sel;
- @sel = grep { ref($_) eq 'ARRAY' } @sel;
- return esc_html($str, %opts) unless @sel;
-
- my $out = '';
- my $pos = 0;
-
- for my $s (@sel) {
- my ($begin, $end) = @$s;
-
- # Don't create empty elements.
- next if $end <= $begin;
-
- my $escaped = esc_html(substr($str, $begin, $end - $begin),
- %opts);
-
- $out .= esc_html(substr($str, $pos, $begin - $pos), %opts)
- if ($begin - $pos > 0);
- $out .= $cgi->span({-class => $css_class}, $escaped);
-
- $pos = $end;
- }
- $out .= esc_html(substr($str, $pos), %opts)
- if ($pos < length($str));
-
- return $out;
-}
-
-# return positions of beginning and end of each match
-sub matchpos_list {
- my ($str, $regexp) = @_;
- return unless (defined $str && defined $regexp);
-
- my @matches;
- while ($str =~ /$regexp/g) {
- push @matches, [$-[0], $+[0]];
- }
- return @matches;
-}
-
-# highlight match (if any), and escape HTML
-sub esc_html_match_hl {
- my ($str, $regexp) = @_;
- return esc_html($str) unless defined $regexp;
-
- my @matches = matchpos_list($str, $regexp);
- return esc_html($str) unless @matches;
-
- return esc_html_hl_regions($str, 'match', @matches);
-}
-
-
-# highlight match (if any) of shortened string, and escape HTML
-sub esc_html_match_hl_chopped {
- my ($str, $chopped, $regexp) = @_;
- return esc_html_match_hl($str, $regexp) unless defined $chopped;
-
- my @matches = matchpos_list($str, $regexp);
- return esc_html($chopped) unless @matches;
-
- # filter matches so that we mark chopped string
- my $tail = "... "; # see chop_str
- unless ($chopped =~ s/\Q$tail\E$//) {
- $tail = '';
- }
- my $chop_len = length($chopped);
- my $tail_len = length($tail);
- my @filtered;
-
- for my $m (@matches) {
- if ($m->[0] > $chop_len) {
- push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
- last;
- } elsif ($m->[1] > $chop_len) {
- push @filtered, [ $m->[0], $chop_len + $tail_len ];
- last;
- }
- push @filtered, $m;
- }
-
- return esc_html_hl_regions($chopped . $tail, 'match', @filtered);
-}
-
-## ----------------------------------------------------------------------
-## functions returning short strings
-
-# CSS class for given age value (in seconds)
-sub age_class {
- my $age = shift;
-
- if (!defined $age) {
- return "noage";
- } elsif ($age < 60*60*2) {
- return "age0";
- } elsif ($age < 60*60*24*2) {
- return "age1";
- } else {
- return "age2";
- }
-}
-
-# convert age in seconds to "nn units ago" string
-sub age_string {
- my $age = shift;
- my $age_str;
-
- if ($age > 60*60*24*365*2) {
- $age_str = (int $age/60/60/24/365);
- $age_str .= " years ago";
- } elsif ($age > 60*60*24*(365/12)*2) {
- $age_str = int $age/60/60/24/(365/12);
- $age_str .= " months ago";
- } elsif ($age > 60*60*24*7*2) {
- $age_str = int $age/60/60/24/7;
- $age_str .= " weeks ago";
- } elsif ($age > 60*60*24*2) {
- $age_str = int $age/60/60/24;
- $age_str .= " days ago";
- } elsif ($age > 60*60*2) {
- $age_str = int $age/60/60;
- $age_str .= " hours ago";
- } elsif ($age > 60*2) {
- $age_str = int $age/60;
- $age_str .= " min ago";
- } elsif ($age > 2) {
- $age_str = int $age;
- $age_str .= " sec ago";
- } else {
- $age_str .= " right now";
- }
- return $age_str;
-}
-
-use constant {
- S_IFINVALID => 0030000,
- S_IFGITLINK => 0160000,
-};
-
-# submodule/subproject, a commit object reference
-sub S_ISGITLINK {
- my $mode = shift;
-
- return (($mode & S_IFMT) == S_IFGITLINK)
-}
-
-# convert file mode in octal to symbolic file mode string
-sub mode_str {
- my $mode = oct shift;
-
- if (S_ISGITLINK($mode)) {
- return 'm---------';
- } elsif (S_ISDIR($mode & S_IFMT)) {
- return 'drwxr-xr-x';
- } elsif (S_ISLNK($mode)) {
- return 'lrwxrwxrwx';
- } elsif (S_ISREG($mode)) {
- # git cares only about the executable bit
- if ($mode & S_IXUSR) {
- return '-rwxr-xr-x';
- } else {
- return '-rw-r--r--';
- };
- } else {
- return '----------';
- }
-}
-
-# convert file mode in octal to file type string
-sub file_type {
- my $mode = shift;
-
- if ($mode !~ m/^[0-7]+$/) {
- return $mode;
- } else {
- $mode = oct $mode;
- }
-
- if (S_ISGITLINK($mode)) {
- return "submodule";
- } elsif (S_ISDIR($mode & S_IFMT)) {
- return "directory";
- } elsif (S_ISLNK($mode)) {
- return "symlink";
- } elsif (S_ISREG($mode)) {
- return "file";
- } else {
- return "unknown";
- }
-}
-
-# convert file mode in octal to file type description string
-sub file_type_long {
- my $mode = shift;
-
- if ($mode !~ m/^[0-7]+$/) {
- return $mode;
- } else {
- $mode = oct $mode;
- }
-
- if (S_ISGITLINK($mode)) {
- return "submodule";
- } elsif (S_ISDIR($mode & S_IFMT)) {
- return "directory";
- } elsif (S_ISLNK($mode)) {
- return "symlink";
- } elsif (S_ISREG($mode)) {
- if ($mode & S_IXUSR) {
- return "executable";
- } else {
- return "file";
- };
- } else {
- return "unknown";
- }
-}
-
-
-## ----------------------------------------------------------------------
-## functions returning short HTML fragments, or transforming HTML fragments
-## which don't belong to other sections
-
-# format line of commit message.
-sub format_log_line_html {
- my $line = shift;
-
- $line = esc_html($line, -nbsp=>1);
- $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
- $cgi->a({-href => href(action=>"object", hash=>$1),
- -class => "text"}, $1);
- }eg;
-
- return $line;
-}
-
-# format marker of refs pointing to given object
-
-# the destination action is chosen based on object type and current context:
-# - for annotated tags, we choose the tag view unless it's the current view
-# already, in which case we go to shortlog view
-# - for other refs, we keep the current view if we're in history, shortlog or
-# log view, and select shortlog otherwise
-sub format_ref_marker {
- my ($refs, $id) = @_;
- my $markers = '';
-
- if (defined $refs->{$id}) {
- foreach my $ref (@{$refs->{$id}}) {
- # this code exploits the fact that non-lightweight tags are the
- # only indirect objects, and that they are the only objects for which
- # we want to use tag instead of shortlog as action
- my ($type, $name) = qw();
- my $indirect = ($ref =~ s/\^\{\}$//);
- # e.g. tags/v2.6.11 or heads/next
- if ($ref =~ m!^(.*?)s?/(.*)$!) {
- $type = $1;
- $name = $2;
- } else {
- $type = "ref";
- $name = $ref;
- }
-
- my $class = $type;
- $class .= " indirect" if $indirect;
-
- my $dest_action = "shortlog";
-
- if ($indirect) {
- $dest_action = "tag" unless $action eq "tag";
- } elsif ($action =~ /^(history|(short)?log)$/) {
- $dest_action = $action;
- }
-
- my $dest = "";
- $dest .= "refs/" unless $ref =~ m!^refs/!;
- $dest .= $ref;
-
- my $link = $cgi->a({
- -href => href(
- action=>$dest_action,
- hash=>$dest
- )}, $name);
-
- $markers .= " " .
- $link . "";
- }
- }
-
- if ($markers) {
- return ' '. $markers . '';
- } else {
- return "";
- }
-}
-
-# format, perhaps shortened and with markers, title line
-sub format_subject_html {
- my ($long, $short, $href, $extra) = @_;
- $extra = '' unless defined($extra);
-
- if (length($short) < length($long)) {
- $long =~ s/[[:cntrl:]]/?/g;
- return $cgi->a({-href => $href, -class => "list subject",
- -title => to_utf8($long)},
- esc_html($short)) . $extra;
- } else {
- return $cgi->a({-href => $href, -class => "list subject"},
- esc_html($long)) . $extra;
- }
-}
-
-# Rather than recomputing the url for an email multiple times, we cache it
-# after the first hit. This gives a visible benefit in views where the avatar
-# for the same email is used repeatedly (e.g. shortlog).
-# The cache is shared by all avatar engines (currently gravatar only), which
-# are free to use it as preferred. Since only one avatar engine is used for any
-# given page, there's no risk for cache conflicts.
-our %avatar_cache = ();
-
-# Compute the picon url for a given email, by using the picon search service over at
-# http://www.cs.indiana.edu/picons/search.html
-sub picon_url {
- my $email = lc shift;
- if (!$avatar_cache{$email}) {
- my ($user, $domain) = split('@', $email);
- $avatar_cache{$email} =
- "//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
- "$domain/$user/" .
- "users+domains+unknown/up/single";
- }
- return $avatar_cache{$email};
-}
-
-# Compute the gravatar url for a given email, if it's not in the cache already.
-# Gravatar stores only the part of the URL before the size, since that's the
-# one computationally more expensive. This also allows reuse of the cache for
-# different sizes (for this particular engine).
-sub gravatar_url {
- my $email = lc shift;
- my $size = shift;
- $avatar_cache{$email} ||=
- "//www.gravatar.com/avatar/" .
- Digest::MD5::md5_hex($email) . "?s=";
- return $avatar_cache{$email} . $size;
-}
-
-# Insert an avatar for the given $email at the given $size if the feature
-# is enabled.
-sub git_get_avatar {
- my ($email, %opts) = @_;
- my $pre_white = ($opts{-pad_before} ? " " : "");
- my $post_white = ($opts{-pad_after} ? " " : "");
- $opts{-size} ||= 'default';
- my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
- my $url = "";
- if ($git_avatar eq 'gravatar') {
- $url = gravatar_url($email, $size);
- } elsif ($git_avatar eq 'picon') {
- $url = picon_url($email);
- }
- # Other providers can be added by extending the if chain, defining $url
- # as needed. If no variant puts something in $url, we assume avatars
- # are completely disabled/unavailable.
- if ($url) {
- return $pre_white .
- "" . $post_white;
- } else {
- return "";
- }
-}
-
-sub format_search_author {
- my ($author, $searchtype, $displaytext) = @_;
- my $have_search = gitweb_check_feature('search');
-
- if ($have_search) {
- my $performed = "";
- if ($searchtype eq 'author') {
- $performed = "authored";
- } elsif ($searchtype eq 'committer') {
- $performed = "committed";
- }
-
- return $cgi->a({-href => href(action=>"search", hash=>$hash,
- searchtext=>$author,
- searchtype=>$searchtype), class=>"list",
- title=>"Search for commits $performed by $author"},
- $displaytext);
-
- } else {
- return $displaytext;
- }
-}
-
-# format the author name of the given commit with the given tag
-# the author name is chopped and escaped according to the other
-# optional parameters (see chop_str).
-sub format_author_html {
- my $tag = shift;
- my $co = shift;
- my $author = chop_and_escape_str($co->{'author_name'}, @_);
- return "<$tag class=\"author\">" .
- format_search_author($co->{'author_name'}, "author",
- git_get_avatar($co->{'author_email'}, -pad_after => 1) .
- $author) .
- "$tag>";
-}
-
-# format git diff header line, i.e. "diff --(git|combined|cc) ..."
-sub format_git_diff_header_line {
- my $line = shift;
- my $diffinfo = shift;
- my ($from, $to) = @_;
-
- if ($diffinfo->{'nparents'}) {
- # combined diff
- $line =~ s!^(diff (.*?) )"?.*$!$1!;
- if ($to->{'href'}) {
- $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
- esc_path($to->{'file'}));
- } else { # file was deleted (no href)
- $line .= esc_path($to->{'file'});
- }
- } else {
- # "ordinary" diff
- $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
- if ($from->{'href'}) {
- $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
- 'a/' . esc_path($from->{'file'}));
- } else { # file was added (no href)
- $line .= 'a/' . esc_path($from->{'file'});
- }
- $line .= ' ';
- if ($to->{'href'}) {
- $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
- 'b/' . esc_path($to->{'file'}));
- } else { # file was deleted
- $line .= 'b/' . esc_path($to->{'file'});
- }
- }
-
- return "
$line
\n";
-}
-
-# format extended diff header line, before patch itself
-sub format_extended_diff_header_line {
- my $line = shift;
- my $diffinfo = shift;
- my ($from, $to) = @_;
-
- # match
- if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
- $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
- esc_path($from->{'file'}));
- }
- if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
- $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
- esc_path($to->{'file'}));
- }
- # match single
- if ($line =~ m/\s(\d{6})$/) {
- $line .= ' (' .
- file_type_long($1) .
- ')';
- }
- # match
- if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
- # can match only for combined diff
- $line = 'index ';
- for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
- if ($from->{'href'}[$i]) {
- $line .= $cgi->a({-href=>$from->{'href'}[$i],
- -class=>"hash"},
- substr($diffinfo->{'from_id'}[$i],0,7));
- } else {
- $line .= '0' x 7;
- }
- # separator
- $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
- }
- $line .= '..';
- if ($to->{'href'}) {
- $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
- substr($diffinfo->{'to_id'},0,7));
- } else {
- $line .= '0' x 7;
- }
-
- } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
- # can match only for ordinary diff
- my ($from_link, $to_link);
- if ($from->{'href'}) {
- $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
- substr($diffinfo->{'from_id'},0,7));
- } else {
- $from_link = '0' x 7;
- }
- if ($to->{'href'}) {
- $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
- substr($diffinfo->{'to_id'},0,7));
- } else {
- $to_link = '0' x 7;
- }
- my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
- $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
- }
-
- return $line . " \n";
-}
-
-# format from-file/to-file diff header
-sub format_diff_from_to_header {
- my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
- my $line;
- my $result = '';
-
- $line = $from_line;
- #assert($line =~ m/^---/) if DEBUG;
- # no extra formatting for "^--- /dev/null"
- if (! $diffinfo->{'nparents'}) {
- # ordinary (single parent) diff
- if ($line =~ m!^--- "?a/!) {
- if ($from->{'href'}) {
- $line = '--- a/' .
- $cgi->a({-href=>$from->{'href'}, -class=>"path"},
- esc_path($from->{'file'}));
- } else {
- $line = '--- a/' .
- esc_path($from->{'file'});
- }
- }
- $result .= qq!
\n"; # class="page_footer"
- }
-
- if (defined $site_footer && -f $site_footer) {
- insert_file($site_footer);
- }
-
- print qq!\n!;
- if (defined $action &&
- $action eq 'blame_incremental') {
- print qq!\n!;
- } else {
- my ($jstimezone, $tz_cookie, $datetime_class) =
- gitweb_get_feature('javascript-timezone');
-
- print qq!\n!;
- }
-
- print "\n" .
- "";
-}
-
-# die_error(, [, ])
-# Example: die_error(404, 'Hash not found')
-# By convention, use the following status codes (as defined in RFC 2616):
-# 400: Invalid or missing CGI parameters, or
-# requested object exists but has wrong type.
-# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
-# this server or project.
-# 404: Requested object/revision/project doesn't exist.
-# 500: The server isn't configured properly, or
-# an internal error occurred (e.g. failed assertions caused by bugs), or
-# an unknown error occurred (e.g. the git binary died unexpectedly).
-# 503: The server is currently unavailable (because it is overloaded,
-# or down for maintenance). Generally, this is a temporary state.
-sub die_error {
- my $status = shift || 500;
- my $error = esc_html(shift) || "Internal Server Error";
- my $extra = shift;
- my %opts = @_;
-
- my %http_responses = (
- 400 => '400 Bad Request',
- 403 => '403 Forbidden',
- 404 => '404 Not Found',
- 500 => '500 Internal Server Error',
- 503 => '503 Service Unavailable',
- );
- git_header_html($http_responses{$status}, undef, %opts);
- print <
-
\n";
-}
-
-# Group output by placing it in a DIV element and adding a header.
-# Options for start_div() can be provided by passing a hash reference as the
-# first parameter to the function.
-# Options to git_print_header_div() can be provided by passing an array
-# reference. This must follow the options to start_div if they are present.
-# The content can be a scalar, which is output as-is, a scalar reference, which
-# is output after html escaping, an IO handle passed either as *handle or
-# *handle{IO}, or a function reference. In the latter case all following
-# parameters will be taken as argument to the content function call.
-sub git_print_section {
- my ($div_args, $header_args, $content);
- my $arg = shift;
- if (ref($arg) eq 'HASH') {
- $div_args = $arg;
- $arg = shift;
- }
- if (ref($arg) eq 'ARRAY') {
- $header_args = $arg;
- $arg = shift;
- }
- $content = $arg;
-
- print $cgi->start_div($div_args);
- git_print_header_div(@$header_args);
-
- if (ref($content) eq 'CODE') {
- $content->(@_);
- } elsif (ref($content) eq 'SCALAR') {
- print esc_html($$content);
- } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
- print <$content>;
- } elsif (!ref($content) && defined($content)) {
- print $content;
- }
-
- print $cgi->end_div;
-}
-
-sub format_timestamp_html {
- my $date = shift;
- my $strtime = $date->{'rfc2822'};
-
- my (undef, undef, $datetime_class) =
- gitweb_get_feature('javascript-timezone');
- if ($datetime_class) {
- $strtime = qq!$strtime!;
- }
-
- my $localtime_format = '(%02d:%02d %s)';
- if ($date->{'hour_local'} < 6) {
- $localtime_format = '(%02d:%02d %s)';
- }
- $strtime .= ' ' .
- sprintf($localtime_format,
- $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
-
- return $strtime;
-}
-
-# Outputs the author name and date in long form
-sub git_print_authorship {
- my $co = shift;
- my %opts = @_;
- my $tag = $opts{-tag} || 'div';
- my $author = $co->{'author_name'};
-
- my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
- print "<$tag class=\"author_date\">" .
- format_search_author($author, "author", esc_html($author)) .
- " [".format_timestamp_html(\%ad)."]".
- git_get_avatar($co->{'author_email'}, -pad_before => 1) .
- "$tag>\n";
-}
-
-# Outputs table rows containing the full author or committer information,
-# in the format expected for 'commit' view (& similar).
-# Parameters are a commit hash reference, followed by the list of people
-# to output information for. If the list is empty it defaults to both
-# author and committer.
-sub git_print_authorship_rows {
- my $co = shift;
- # too bad we can't use @people = @_ || ('author', 'committer')
- my @people = @_;
- @people = ('author', 'committer') unless @people;
- foreach my $who (@people) {
- my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
- print "
\n";
-}
-
-sub git_print_log {
- my $log = shift;
- my %opts = @_;
-
- if ($opts{'-remove_title'}) {
- # remove title, i.e. first line of log
- shift @$log;
- }
- # remove leading empty lines
- while (defined $log->[0] && $log->[0] eq "") {
- shift @$log;
- }
-
- # print log
- my $skip_blank_line = 0;
- foreach my $line (@$log) {
- if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) {
- if (! $opts{'-remove_signoff'}) {
- print "" . esc_html($line) . " \n";
- $skip_blank_line = 1;
- }
- next;
- }
-
- if ($line =~ m,\s*([a-z]*link): (https?://\S+),i) {
- if (! $opts{'-remove_signoff'}) {
- print "" . esc_html($1) . ": " .
- "" . esc_html($2) . "" .
- " \n";
- $skip_blank_line = 1;
- }
- next;
- }
-
- # print only one empty line
- # do not print empty line after signoff
- if ($line eq "") {
- next if ($skip_blank_line);
- $skip_blank_line = 1;
- } else {
- $skip_blank_line = 0;
- }
-
- print format_log_line_html($line) . " \n";
- }
-
- if ($opts{'-final_empty_line'}) {
- # end with single empty line
- print " \n" unless $skip_blank_line;
- }
-}
-
-# return link target (what link points to)
-sub git_get_link_target {
- my $hash = shift;
- my $link_target;
-
- # read link
- open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
- or return;
- {
- local $/ = undef;
- $link_target = <$fd>;
- }
- close $fd
- or return;
-
- return $link_target;
-}
-
-# given link target, and the directory (basedir) the link is in,
-# return target of link relative to top directory (top tree);
-# return undef if it is not possible (including absolute links).
-sub normalize_link_target {
- my ($link_target, $basedir) = @_;
-
- # absolute symlinks (beginning with '/') cannot be normalized
- return if (substr($link_target, 0, 1) eq '/');
-
- # normalize link target to path from top (root) tree (dir)
- my $path;
- if ($basedir) {
- $path = $basedir . '/' . $link_target;
- } else {
- # we are in top (root) tree (dir)
- $path = $link_target;
- }
-
- # remove //, /./, and /../
- my @path_parts;
- foreach my $part (split('/', $path)) {
- # discard '.' and ''
- next if (!$part || $part eq '.');
- # handle '..'
- if ($part eq '..') {
- if (@path_parts) {
- pop @path_parts;
- } else {
- # link leads outside repository (outside top dir)
- return;
- }
- } else {
- push @path_parts, $part;
- }
- }
- $path = join('/', @path_parts);
-
- return $path;
-}
-
-# print tree entry (row of git_tree), but without encompassing
element
-sub git_print_tree_entry {
- my ($t, $basedir, $hash_base, $have_blame) = @_;
-
- my %base_key = ();
- $base_key{'hash_base'} = $hash_base if defined $hash_base;
-
- # The format of a table row is: mode list link. Where mode is
- # the mode of the entry, list is the name of the entry, an href,
- # and link is the action links of the entry.
-
- print "
';
- }
-}
-
-# Print context lines and then rem/add lines in inline manner.
-sub print_inline_diff_lines {
- my ($ctx, $rem, $add) = @_;
-
- print @$ctx, @$rem, @$add;
-}
-
-# Format removed and added line, mark changed part and HTML-format them.
-# Implementation is based on contrib/diff-highlight
-sub format_rem_add_lines_pair {
- my ($rem, $add, $num_parents) = @_;
-
- # We need to untabify lines before split()'ing them;
- # otherwise offsets would be invalid.
- chomp $rem;
- chomp $add;
- $rem = untabify($rem);
- $add = untabify($add);
-
- my @rem = split(//, $rem);
- my @add = split(//, $add);
- my ($esc_rem, $esc_add);
- # Ignore leading +/- characters for each parent.
- my ($prefix_len, $suffix_len) = ($num_parents, 0);
- my ($prefix_has_nonspace, $suffix_has_nonspace);
-
- my $shorter = (@rem < @add) ? @rem : @add;
- while ($prefix_len < $shorter) {
- last if ($rem[$prefix_len] ne $add[$prefix_len]);
-
- $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
- $prefix_len++;
- }
-
- while ($prefix_len + $suffix_len < $shorter) {
- last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
-
- $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
- $suffix_len++;
- }
-
- # Mark lines that are different from each other, but have some common
- # part that isn't whitespace. If lines are completely different, don't
- # mark them because that would make output unreadable, especially if
- # diff consists of multiple lines.
- if ($prefix_has_nonspace || $suffix_has_nonspace) {
- $esc_rem = esc_html_hl_regions($rem, 'marked',
- [$prefix_len, @rem - $suffix_len], -nbsp=>1);
- $esc_add = esc_html_hl_regions($add, 'marked',
- [$prefix_len, @add - $suffix_len], -nbsp=>1);
- } else {
- $esc_rem = esc_html($rem, -nbsp=>1);
- $esc_add = esc_html($add, -nbsp=>1);
- }
-
- return format_diff_line(\$esc_rem, 'rem'),
- format_diff_line(\$esc_add, 'add');
-}
-
-# HTML-format diff context, removed and added lines.
-sub format_ctx_rem_add_lines {
- my ($ctx, $rem, $add, $num_parents) = @_;
- my (@new_ctx, @new_rem, @new_add);
- my $can_highlight = 0;
- my $is_combined = ($num_parents > 1);
-
- # Highlight if every removed line has a corresponding added line.
- if (@$add > 0 && @$add == @$rem) {
- $can_highlight = 1;
-
- # Highlight lines in combined diff only if the chunk contains
- # diff between the same version, e.g.
- #
- # - a
- # - b
- # + c
- # + d
- #
- # Otherwise the highlightling would be confusing.
- if ($is_combined) {
- for (my $i = 0; $i < @$add; $i++) {
- my $prefix_rem = substr($rem->[$i], 0, $num_parents);
- my $prefix_add = substr($add->[$i], 0, $num_parents);
-
- $prefix_rem =~ s/-/+/g;
-
- if ($prefix_rem ne $prefix_add) {
- $can_highlight = 0;
- last;
- }
- }
- }
- }
-
- if ($can_highlight) {
- for (my $i = 0; $i < @$add; $i++) {
- my ($line_rem, $line_add) = format_rem_add_lines_pair(
- $rem->[$i], $add->[$i], $num_parents);
- push @new_rem, $line_rem;
- push @new_add, $line_add;
- }
- } else {
- @new_rem = map { format_diff_line($_, 'rem') } @$rem;
- @new_add = map { format_diff_line($_, 'add') } @$add;
- }
-
- @new_ctx = map { format_diff_line($_, 'ctx') } @$ctx;
-
- return (\@new_ctx, \@new_rem, \@new_add);
-}
-
-# Print context lines and then rem/add lines.
-sub print_diff_lines {
- my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
- my $is_combined = $num_parents > 1;
-
- ($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add,
- $num_parents);
-
- if ($diff_style eq 'sidebyside' && !$is_combined) {
- print_sidebyside_diff_lines($ctx, $rem, $add);
- } else {
- # default 'inline' style and unknown styles
- print_inline_diff_lines($ctx, $rem, $add);
- }
-}
-
-sub print_diff_chunk {
- my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
- my (@ctx, @rem, @add);
-
- # The class of the previous line.
- my $prev_class = '';
-
- return unless @chunk;
-
- # incomplete last line might be among removed or added lines,
- # or both, or among context lines: find which
- for (my $i = 1; $i < @chunk; $i++) {
- if ($chunk[$i][0] eq 'incomplete') {
- $chunk[$i][0] = $chunk[$i-1][0];
- }
- }
-
- # guardian
- push @chunk, ["", ""];
-
- foreach my $line_info (@chunk) {
- my ($class, $line) = @$line_info;
-
- # print chunk headers
- if ($class && $class eq 'chunk_header') {
- print format_diff_line($line, $class, $from, $to);
- next;
- }
-
- ## print from accumulator when have some add/rem lines or end
- # of chunk (flush context lines), or when have add and rem
- # lines and new block is reached (otherwise add/rem lines could
- # be reordered)
- if (!$class || ((@rem || @add) && $class eq 'ctx') ||
- (@rem && @add && $class ne $prev_class)) {
- print_diff_lines(\@ctx, \@rem, \@add,
- $diff_style, $num_parents);
- @ctx = @rem = @add = ();
- }
-
- ## adding lines to accumulator
- # guardian value
- last unless $line;
- # rem, add or change
- if ($class eq 'rem') {
- push @rem, $line;
- } elsif ($class eq 'add') {
- push @add, $line;
- }
- # context line
- if ($class eq 'ctx') {
- push @ctx, $line;
- }
-
- $prev_class = $class;
- }
-}
-
-sub git_patchset_body {
- my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
- my ($hash_parent) = $hash_parents[0];
-
- my $is_combined = (@hash_parents > 1);
- my $patch_idx = 0;
- my $patch_number = 0;
- my $patch_line;
- my $diffinfo;
- my $to_name;
- my (%from, %to);
- my @chunk; # for side-by-side diff
-
- print "
\n";
-
- # skip to first patch
- while ($patch_line = <$fd>) {
- chomp $patch_line;
-
- last if ($patch_line =~ m/^diff /);
- }
-
- PATCH:
- while ($patch_line) {
-
- # parse "git diff" header line
- if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
- # $1 is from_name, which we do not use
- $to_name = unquote($2);
- $to_name =~ s!^b/!!;
- } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
- # $1 is 'cc' or 'combined', which we do not use
- $to_name = unquote($2);
- } else {
- $to_name = undef;
- }
-
- # check if current patch belong to current raw line
- # and parse raw git-diff line if needed
- if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
- # this is continuation of a split patch
- print "
\n";
- } else {
- # advance raw git-diff output if needed
- $patch_idx++ if defined $diffinfo;
-
- # read and prepare patch information
- $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
-
- # compact combined diff output can have some patches skipped
- # find which patch (using pathname of result) we are at now;
- if ($is_combined) {
- while ($to_name ne $diffinfo->{'to_file'}) {
- print "
\n"; # class="patch"
-
- $patch_idx++;
- $patch_number++;
-
- last if $patch_idx > $#$difftree;
- $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
- }
- }
-
- # modifies %from, %to hashes
- parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
-
- # this is first patch for raw difftree line with $patch_idx index
- # we index @$difftree array from 0, but number patches from 1
- print "
\n"; # class="patch"
- }
-
- # for compact combined (--cc) format, with chunk and patch simplification
- # the patchset might be empty, but there might be unprocessed raw lines
- for (++$patch_idx if $patch_number > 0;
- $patch_idx < @$difftree;
- ++$patch_idx) {
- # read and prepare patch information
- $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
-
- # generate anchor for "patch" links in difftree / whatchanged part
- print "
\n";
- }
-
- # use per project git URL list in $projectroot/$project/cloneurl
- # or make project git URL from git base URL and project name
- my $url_tag = "URL";
- my @url_list = git_get_project_url_list($project);
- @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
- foreach my $git_url (@url_list) {
- next unless $git_url;
- print format_repo_url($url_tag, $git_url);
- $url_tag = "";
- }
-
- # Tag cloud
- my $show_ctags = gitweb_check_feature('ctags');
- if ($show_ctags) {
- my $ctags = git_get_project_ctags($project);
- if (%$ctags) {
- # without ability to add tags, don't show if there are none
- my $cloud = git_populate_project_tagcloud($ctags);
- print "
" .
- "
content tags
" .
- "
".git_show_project_tagcloud($cloud, 48)."
" .
- "
\n";
- }
- }
-
- print "
\n";
-
- # If XSS prevention is on, we don't include README.html.
- # TODO: Allow a readme in some safe format.
- if (!$prevent_xss && -s "$projectroot/$project/README.html") {
- print "
";
- git_footer_html();
-}
-
-sub git_tree {
- if (!defined $hash_base) {
- $hash_base = "HEAD";
- }
- if (!defined $hash) {
- if (defined $file_name) {
- $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
- } else {
- $hash = $hash_base;
- }
- }
- die_error(404, "No such tree") unless defined($hash);
-
- my $show_sizes = gitweb_check_feature('show-sizes');
- my $have_blame = gitweb_check_feature('blame');
-
- my @entries = ();
- {
- local $/ = "\0";
- open my $fd, "-|", git_cmd(), "ls-tree", '-z',
- ($show_sizes ? '-l' : ()), @extra_options, $hash
- or die_error(500, "Open git-ls-tree failed");
- @entries = map { chomp; $_ } <$fd>;
- close $fd
- or die_error(404, "Reading tree failed");
- }
-
- my $refs = git_get_references();
- my $ref = format_ref_marker($refs, $hash_base);
- git_header_html();
- my $basedir = '';
- if (defined $hash_base && (my %co = parse_commit($hash_base))) {
- my @views_nav = ();
- if (defined $file_name) {
- push @views_nav,
- $cgi->a({-href => href(action=>"history", -replay=>1)},
- "history"),
- $cgi->a({-href => href(action=>"tree",
- hash_base=>"HEAD", file_name=>$file_name)},
- "HEAD"),
- }
- my $snapshot_links = format_snapshot_links($hash);
- if (defined $snapshot_links) {
- # FIXME: Should be available when we have no hash base as well.
- push @views_nav, $snapshot_links;
- }
- git_print_page_nav('tree','', $hash_base, undef, undef,
- join(' | ', @views_nav));
- git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
- } else {
- undef $hash_base;
- print "
\n";
- print "
\n";
- print "
".esc_html($hash)."
\n";
- }
- if (defined $file_name) {
- $basedir = $file_name;
- if ($basedir ne '' && substr($basedir, -1) ne '/') {
- $basedir .= '/';
- }
- git_print_page_path($file_name, 'tree', $hash_base);
- }
- print "
\n";
- print "
\n";
- my $alternate = 1;
- # '..' (top directory) link if possible
- if (defined $hash_base &&
- defined $file_name && $file_name =~ m![^/]+$!) {
- if ($alternate) {
- print "
\n";
- } else {
- print "
\n";
- }
- $alternate ^= 1;
-
- my $up = $file_name;
- $up =~ s!/?[^/]+$!!;
- undef $up unless $up;
- # based on git_print_tree_entry
- print '
\n"; # class="page_body"
- git_footer_html();
-
- } elsif ($format eq 'plain') {
- local $/ = undef;
- print <$fd>;
- close $fd
- or print "Reading git-diff-tree failed\n";
- } elsif ($format eq 'patch') {
- local $/ = undef;
- print <$fd>;
- close $fd
- or print "Reading git-format-patch failed\n";
- }
-}
-
-sub git_commitdiff_plain {
- git_commitdiff(-format => 'plain');
-}
-
-# format-patch-style patches
-sub git_patch {
- git_commitdiff(-format => 'patch', -single => 1);
-}
-
-sub git_patches {
- git_commitdiff(-format => 'patch');
-}
-
-sub git_history {
- git_log_generic('history', \&git_history_body,
- $hash_base, $hash_parent_base,
- $file_name, $hash);
-}
-
-sub git_search {
- $searchtype ||= 'commit';
-
- # check if appropriate features are enabled
- gitweb_check_feature('search')
- or die_error(403, "Search is disabled");
- if ($searchtype eq 'pickaxe') {
- # pickaxe may take all resources of your box and run for several minutes
- # with every query - so decide by yourself how public you make this feature
- gitweb_check_feature('pickaxe')
- or die_error(403, "Pickaxe search is disabled");
- }
- if ($searchtype eq 'grep') {
- # grep search might be potentially CPU-intensive, too
- gitweb_check_feature('grep')
- or die_error(403, "Grep search is disabled");
- }
-
- if (!defined $searchtext) {
- die_error(400, "Text field is empty");
- }
- if (!defined $hash) {
- $hash = git_get_head_hash($project);
- }
- my %co = parse_commit($hash);
- if (!%co) {
- die_error(404, "Unknown commit object");
- }
- if (!defined $page) {
- $page = 0;
- }
-
- if ($searchtype eq 'commit' ||
- $searchtype eq 'author' ||
- $searchtype eq 'committer') {
- git_search_message(%co);
- } elsif ($searchtype eq 'pickaxe') {
- git_search_changes(%co);
- } elsif ($searchtype eq 'grep') {
- git_search_files(%co);
- } else {
- die_error(400, "Unknown search type");
- }
-}
-
-sub git_search_help {
- git_header_html();
- git_print_page_nav('','', $hash,$hash,$hash);
- print <Pattern is by default a normal string that is matched precisely (but without
-regard to case, except in the case of pickaxe). However, when you check the re checkbox,
-the pattern entered is recognized as the POSIX extended
-regular expression (also case
-insensitive).
-
-
commit
-
The commit messages and authorship information will be scanned for the given pattern.
-EOT
- my $have_grep = gitweb_check_feature('grep');
- if ($have_grep) {
- print <grep
-
All files in the currently selected tree (HEAD unless you are explicitly browsing
- a different one) are searched for the given pattern. On large trees, this search can take
-a while and put some strain on the server, so please use it with some consideration. Note that
-due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
-case-sensitive.
-EOT
- }
- print <author
-
Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.
-
committer
-
Name and e-mail of the committer and date of commit will be scanned for the given pattern.
-EOT
- my $have_pickaxe = gitweb_check_feature('pickaxe');
- if ($have_pickaxe) {
- print <pickaxe
-
All commits that caused the string to appear or disappear from any file (changes that
-added, removed or "modified" the string) will be listed. This search can take a while and
-takes a lot of strain on the server, so please use it wisely. Note that since you may be
-interested even in changes just changing the case as well, this search is case sensitive.
-EOT
- }
- print "
\n";
- git_footer_html();
-}
-
-sub git_shortlog {
- git_log_generic('shortlog', \&git_shortlog_body,
- $hash, $hash_parent);
-}
-
-## ......................................................................
-## feeds (RSS, Atom; OPML)
-
-sub git_feed {
- my $format = shift || 'atom';
- my $have_blame = gitweb_check_feature('blame');
-
- # Atom: http://www.atomenabled.org/developers/syndication/
- # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
- if ($format ne 'rss' && $format ne 'atom') {
- die_error(400, "Unknown web feed format");
- }
-
- # log/feed of current (HEAD) branch, log of given branch, history of file/directory
- my $head = $hash || 'HEAD';
- my @commitlist = parse_commits($head, 150, 0, $file_name);
-
- my %latest_commit;
- my %latest_date;
- my $content_type = "application/$format+xml";
- if (defined $cgi->http('HTTP_ACCEPT') &&
- $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
- # browser (feed reader) prefers text/xml
- $content_type = 'text/xml';
- }
- if (defined($commitlist[0])) {
- %latest_commit = %{$commitlist[0]};
- my $latest_epoch = $latest_commit{'committer_epoch'};
- exit_if_unmodified_since($latest_epoch);
- %latest_date = parse_date($latest_epoch, $latest_commit{'committer_tz'});
- }
- print $cgi->header(
- -type => $content_type,
- -charset => 'utf-8',
- %latest_date ? (-last_modified => $latest_date{'rfc2822'}) : (),
- -status => '200 OK');
-
- # Optimization: skip generating the body if client asks only
- # for Last-Modified date.
- return if ($cgi->request_method() eq 'HEAD');
-
- # header variables
- my $title = "$site_name - $project/$action";
- my $feed_type = 'log';
- if (defined $hash) {
- $title .= " - '$hash'";
- $feed_type = 'branch log';
- if (defined $file_name) {
- $title .= " :: $file_name";
- $feed_type = 'history';
- }
- } elsif (defined $file_name) {
- $title .= " - $file_name";
- $feed_type = 'history';
- }
- $title .= " $feed_type";
- $title = esc_html($title);
- my $descr = git_get_project_description($project);
- if (defined $descr) {
- $descr = esc_html($descr);
- } else {
- $descr = "$project " .
- ($format eq 'rss' ? 'RSS' : 'Atom') .
- " feed";
- }
- my $owner = git_get_project_owner($project);
- $owner = esc_html($owner);
-
- #header
- my $alt_url;
- if (defined $file_name) {
- $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
- } elsif (defined $hash) {
- $alt_url = href(-full=>1, action=>"log", hash=>$hash);
- } else {
- $alt_url = href(-full=>1, action=>"summary");
- }
- print qq!\n!;
- if ($format eq 'rss') {
- print <
-
-XML
- print "$title\n" .
- "$alt_url\n" .
- "$descr\n" .
- "en\n" .
- # project owner is responsible for 'editorial' content
- "$owner\n";
- if (defined $logo || defined $favicon) {
- # prefer the logo to the favicon, since RSS
- # doesn't allow both
- my $img = esc_url($logo || $favicon);
- print "\n" .
- "$img\n" .
- "$title\n" .
- "$alt_url\n" .
- "\n";
- }
- if (%latest_date) {
- print "$latest_date{'rfc2822'}\n";
- print "$latest_date{'rfc2822'}\n";
- }
- print "gitweb v.$version/$git_version\n";
- } elsif ($format eq 'atom') {
- print <
-XML
- print "$title\n" .
- "$descr\n" .
- '' . "\n" .
- '' . "\n" .
- "" . href(-full=>1) . "\n" .
- # use project owner for feed author
- "$owner\n";
- if (defined $favicon) {
- print "" . esc_url($favicon) . "\n";
- }
- if (defined $logo) {
- # not twice as wide as tall: 72 x 27 pixels
- print "" . esc_url($logo) . "\n";
- }
- if (! %latest_date) {
- # dummy date to keep the feed valid until commits trickle in:
- print "1970-01-01T00:00:00Z\n";
- } else {
- print "$latest_date{'iso-8601'}\n";
- }
- print "gitweb\n";
- }
-
- # contents
- for (my $i = 0; $i <= $#commitlist; $i++) {
- my %co = %{$commitlist[$i]};
- my $commit = $co{'id'};
- # we read 150, we always show 30 and the ones more recent than 48 hours
- if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
- last;
- }
- my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});
-
- # get list of changed files
- open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
- $co{'parent'} || "--root",
- $co{'id'}, "--", (defined $file_name ? $file_name : ())
- or next;
- my @difftree = map { chomp; $_ } <$fd>;
- close $fd
- or next;
-
- # print element (entry, item)
- my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
- if ($format eq 'rss') {
- print "\n" .
- "" . esc_html($co{'title'}) . "\n" .
- "" . esc_html($co{'author'}) . "\n" .
- "$cd{'rfc2822'}\n" .
- "$co_url\n" .
- "$co_url\n" .
- "" . esc_html($co{'title'}) . "\n" .
- "" .
- "\n" .
- "" . esc_html($co{'title'}) . "\n" .
- "$cd{'iso-8601'}\n" .
- "\n" .
- " " . esc_html($co{'author_name'}) . "\n";
- if ($co{'author_email'}) {
- print " " . esc_html($co{'author_email'}) . "\n";
- }
- print "\n" .
- # use committer for contributor
- "\n" .
- " " . esc_html($co{'committer_name'}) . "\n";
- if ($co{'committer_email'}) {
- print " " . esc_html($co{'committer_email'}) . "\n";
- }
- print "\n" .
- "$cd{'iso-8601'}\n" .
- "\n" .
- "$co_url\n" .
- "\n" .
- "