#!/usr/bin/perl

# apt-repo -- Manipulate APT repository list

# Copyright 2011-2023 by Andrey Cherepanov (cas@altlinux.org)
# Copyright 2015 by Ivan Zakharyaschev (imz@altlinux.org)
# (imz: support for relying on apt-config and APT_CONFIG)
# (imz: Support for local --hsh-apt-config=FILE together with hasher.)

# This program is free software; you can redistribute it and/or modify it
# under the terms of GNU General Public License (GPL) version 3 or later.

use strict;
use warnings;

# Default parameters
our $VERSION = '1.4.4';

my $type     = 'rpm';
my $c_branch = 'classic';
my $c_task   = 'task';
my $noarch   = 'noarch';

my $cmd      = 'list';
my $continues = 0;
my $hsh = 0;

my $new_format_parts = 2;
my $dry_run = 0;
my $use_arepo = check_ignore_arepo();

my $proxy = get_proxy();

if( grep( /^--dry-run$/, @ARGV) ) {
    $dry_run = 1;
    @ARGV = map( /^--dry-run$/ ? () : $_, @ARGV);
}

if ( scalar @ARGV > 0 and $ARGV[0] =~ /^--hsh-apt-config=(.*)$/ ) {
    # Printing is useful for debugging, too:
    warn "Info: Will use hasher when appropriate (with --apt-config=$1).\n";
    $ENV{APT_CONFIG} = $1;
    $hsh = 1;
    shift;
}
$cmd = $ARGV[0] if scalar @ARGV > 0;

# Get system arch
my $arch = `/bin/uname -m`;
chomp $arch; # Truncate carriage return from output
# arch for x86_32
$arch = 'i586' if $arch =~ /^i686$/;

# arch for armh
$arch = 'armh' if $arch =~ /^armv7l$/;

# Default repository paths
my $repo_base = 'http://ftp.altlinux.org/pub/distributions/ALTLinux';
my $repo_archive_base = 'http://ftp.altlinux.org/pub/distributions/archive/';
my $repo_task = 'http://git.altlinux.org/repo/';
my $conf_main = '/etc/apt/sources.list';
my $conf_list = '/etc/apt/sources.list.d/*.list';
if ( defined $ENV{APT_CONFIG} ) {
    # The following expression is intentionally crazily complex,
    # because we want to debug how children see the environment variable.
    warn 'Info: Will try to read a non-system APT_CONFIG=' . `echo -nE "\$APT_CONFIG"` . '\n';
    if ( $ENV{APT_CONFIG} =~ /^~/ ) {
        warn 'Warning: Your APT_CONFIG begins with ~, but no tilde expansion will take place\n';
    }
}
# If APT is missing in the system, we fallback to the common defaults above.
# Otherwise, we get the paths from APT itself:
# (if `apt-config` is missing, the pattern below won't match.)
if ( `apt-config shell FILE Dir::Etc::sourcelist/f` =~ /^FILE=(.*)$/ ) {
    # We are lucky that `apt-config` has the /f and /d "switches" which
    # make it resolve the configuration parameters into "normalized"
    # full paths to files and dirs.

    # $1 is a string quoted for the shell (with quotation marks around it).
    # Therefore we rely on shell itself to print it cleanly:
    $conf_main = `echo -nE $1`;
    # Invalidate the default because we want consistent values from `apt-config`:
    $conf_list = '';
    if ( `apt-config shell DIR Dir::Etc::sourceparts/d` =~ /^DIR=(.*)$/ ) {
        $conf_list = `echo -nE $1` . "*.list";
    } else {
        warn "Warning: Getting Dir::Etc::sourceparts/d from `apt-config` went wrong; falling back to '$conf_list' instead.\n"
    }
} else {
    warn "Warning: Getting Dir::Etc::sourcelist/f from `apt-config` went wrong; falling back to '$conf_main' instead.\n"
}

my %branches  = (
    '4.0' => [ "$repo_base/4.0/branch", "updates", "classic" ],
    '4.1' => [ "$repo_base/4.1/branch", "updates", "classic" ],
    '5.0' => [ "$repo_base/5.0/branch", "updates", "classic" ],
    'p5'  => [ "$repo_base/p5/branch",  "updates", "classic" ],
    '5.1' => [ "$repo_base/5.1/branch", "updates", "classic" ],
    'p6'  => [ "$repo_base/p6/branch",  "updates", "classic" ],
    't6'  => [ "$repo_base/t6/branch",  "updates", "classic" ],
    'c6'  => [ "$repo_base/c6/branch",  "updates", "classic" ],
    'p7'  => [ "$repo_base/p7/branch",  "updates", "classic" ],
    't7'  => [ "$repo_base/t7/branch",  "updates", "classic" ],
    'c7'  => [ "$repo_base/c7/branch",  "updates", "classic" ],
    'p8'  => [ "$repo_base/p8/branch",  "updates", "classic" ],
    'c8'  => [ "http://update.altsp.su/pub/distributions/ALTLinux/c8/branch", "cert8", "classic" ],
    'c8.1' => [ "$repo_base/c8.1/branch",  "updates", "classic" ],
    'p9'  => [ "$repo_base/p9/branch",  "p9", "classic" ],
    'p10'  => [ "$repo_base/p10/branch",  "p10", "classic" ],
    'p11'  => [ "$repo_base/p11/branch",  "p11", "classic" ],
    'sisyphus' => [ "$repo_base/Sisyphus", "alt", "classic" ],
    'Sisyphus' => [ "$repo_base/Sisyphus", "alt", "classic" ],
    'autoimports.p7' => [ "http://ftp.altlinux.ru/pub/distributions/ALTLinux/autoimports/p7", "", "autoimports" ],
    'autoimports.p8' => [ "http://ftp.altlinux.ru/pub/distributions/ALTLinux/autoimports/p8", "", "autoimports" ],
    'autoimports.p9' => [ "http://ftp.altlinux.ru/pub/distributions/ALTLinux/autoimports/p9", "", "autoimports" ],
    'autoimports.p10' => [ "http://ftp.altlinux.ru/pub/distributions/ALTLinux/autoimports/p10", "", "autoimports" ],
    'autoimports.sisyphus' => [ "http://ftp.altlinux.ru/pub/distributions/ALTLinux/autoimports/Sisyphus", "", "autoimports" ],
    'altlinuxclub.4.0' => [ "http://altlinuxclub.ru/repo/Repo_4/",  "", "hasher" ],
    'altlinuxclub.p5'  => [ "http://altlinuxclub.ru/repo/Repo_P5/", "", "hasher" ],
    'altlinuxclub.p6'  => [ "http://altlinuxclub.ru/repo/Repo_P6/", "", "hasher" ],
    'altlinuxclub.p7'  => [ "http://altlinuxclub.ru/repo/Repo_P7/", "", "hasher" ],
    'altlinuxclub.p8'  => [ "http://altlinuxclub.ru/repo/Repo_P8/", "", "hasher" ],
    'altlinuxclub.p9'  => [ "http://altlinuxclub.ru/repo/Repo_P9/", "", "hasher" ],
    'altlinuxclub.p10' => [ "http://altlinuxclub.ru/repo/Repo_P10/", "", "hasher" ],
    'altlinuxclub.sisyphus' => [ "http://altlinuxclub.ru/repo/repo_s/",  "", "hasher" ]
);

my @archiving_branches = (
    'p7',
    'p8',
    'p9',
    'p10',
    't7',
    'sisyphus'
);

sub get_proxy {

    my @res=`apt-config dump | grep -i "https\\?::proxy"`;

    if (scalar @res > 1) {
        warn "Too many proxy found. You should check the apt's configuration.\n";
    }

    if ( defined $res[0] and $res[0] =~ /.*https?::proxy "(.*)";/i ) {
        #print "$1\n";
        return "--proxy $1";
    }

    return "";
}

# Check Arepo ignoring
sub check_ignore_arepo {
    open( my $file, "<", "/etc/sysconfig/apt-repo" ) or return 1;
    while( my $l = <$file> ) {
        if( $l =~ /^AREPO\s*=\s*NO/ ) {
            close( $file );
            return 0;
        }
    }
    close( $file );
    return 1;
}

# Show usage information
sub show_usage {
    print <<"HELP";
Usage: apt-repo [--hsh-apt-config=FILE] [--dry-run] COMMAND SOURCE
Manipulate APT repository list.

COMMANDS:
  list [-a]        List active or all repositories
  list [task] <id> List task packages
  add <source>     Add APT repository
  set <branch>     Remove all exising sources and add branch <branch>
  rm <source|all>  Remove APT repository
  clean            Remove all cdrom and task sources
  update           Update APT caches
                   (Runs `hsh --initroot-only` if `--hsh-apt-config` is given.)
  test [task] <id> [pkg1 ...] Install all packages (except *-devel, *-checkinstall and *-debuginfo) from task <id>
                   (Uses hasher(7) if `--hsh-apt-config` is given.)
                   You can optional specify package(s) to test.
  test [task] '' pkg1 ... Install the packages (without modifying APT repos)
                   (Uses hasher(7) if `--hsh-apt-config` is given.)
  upgrade [task [task...] Upgrade all packages by apt-get dist-upgrade.
                   If task (one or more) is specified, upgrade is performed including this tasks.
  -h, --help       Show help and exit
  -v               Show version and exit

<source> may be branch or task name, sources.list(5) string, URL or local path.

The files to be manipulated are determined by a call to apt-config(8).
Consequently, APT_CONFIG environment variable can be used to point
to arbitrary non-system configurations for APT.

There is a switch for the special case when you want to use hasher(7) together
with a local APT configuration: --hsh-apt-config=FILE.
Note that no tilde expansion is performed on FILE!

If --dry-run is defined in command line changes only shown, is not performed.

If AREPO=NO is uncommented in /etc/sysconfig/apt-repo all additional operations
ignore Arepo sources.
HELP
    exit 0;
}

# Show version
sub show_version {
    print "$VERSION\n";
    exit 0;
}

# Return array of file names with sources
sub get_source_files {
    my $dir;
    my @files = ();

    # Build files list
    push @files, $conf_main if -e $conf_main;
    $dir = substr( $conf_list, 0, -6 );
    opendir( DIR, substr( $conf_list, 0, -6 ) );
    foreach my $name (sort readdir( DIR )) {
        push @files, $dir . $name if $name =~ /\.list$/;
    }
    closedir( DIR );
    return @files;
}

# Return list of repositories as text
sub get_repos {
    my $all = shift;
    my @output = ();
    my $name;

    # Iterate through @files
    foreach $name ( get_source_files() ) {
        open(my $file, "<", $name) or warn "Can't open file '$name'\n";
        while( my $l = <$file> ) {
            # Show all active repositories
            push( @output, $l ) if $l =~ /^[[:space:]]*[^[:space:]#]+/;
            # On -a show all available commented repositories
            push( @output, $l ) if defined $all and $all =~ /^-a$/ and $l =~ /^[[:space:]]*#[[:space:]]*$type(-src|-dir)? /;
        }
        close( $file );
    }
    return @output;
}

# Get package list for task
sub get_task_content {
    my $task = shift;
    my @out = ();

    die "Missing or wrong task number" if ! defined $task or ! $task=~ /^(\d+)$/;

    die "Task $task is unknown or still building" if ! task_exists( $task );

    open P, '-|', "curl $proxy -sL http://git.altlinux.org/tasks/$task/plan/add-bin | cut -f1 | grep -E -v '\\-(devel.*|checkinstall|debuginfo)\$' | sort -u";
    @out = <P>;
    close P;
    return @out;
}

# Get status of task
sub task_exists {
    my $task = shift;
    my @out = ();

    open P, '-|', "curl $proxy -sL  -w '%{http_code}' http://git.altlinux.org/tasks/$task/plan/add-bin";
    @out = <P>;
    close P;
    return ( (pop @out) eq "200" and (scalar @out) != 0);
}

# Get status of arepo for task
sub task_has_arepo {
    my $task = shift;
    my @out = ();

    die "Missing or wrong task number" if ! defined $task or ! $task=~ /^(\d+)$/;

    open P, '-|', "curl $proxy -sL  -w '%{http_code}' http://git.altlinux.org/tasks/$task/plan/arepo-add-x86_64-i586";
    @out = <P>;
    close P;
    return ( (pop @out) eq "200" and (scalar @out) != 0);
}

# Show repositories
sub show_repo {
    shift;
    my $all = shift;

    # List for task packages by task number or canonical form `task <number>`
    if( defined $all and $all =~ /^(\d+|task)$/ ) {
        $all = shift if $all =~ /^task$/;
        print join( "",get_task_content( $all ) );
    } else {
        print get_repos( $all );
    }

    exit 0;
}

# Convert source to new format to show last the dir in apt-get output
sub new_format {
    my $source = shift;
    my @s = split ' ', $source;
    my $i = 1;
    my $path_str;
    my @path;
    my $part;

    # move two last path components to architecture
    $i++ if $s[$i] =~ /^\[/;

    # Set new format only URL contains at least 3 parts
    $path_str = $s[$i];
    $path_str =~ s/^[a-z]+:\/\///;
    @path = split '/', $path_str;

    if( scalar @path > $new_format_parts ) {
        if( $new_format_parts == 3 ) {
            $s[$i] =~ s/(.*)\/([^\/]+\/[^\/]+\/[^\/]+)\/?/${1}/;
            $part = $2;
        } else {
            $s[$i] =~ s/(.*)\/([^\/]+\/[^\/]+)\/?/${1}/;
            $part = $2;
        }
        if( defined $part and $part ne '') {
            $s[$i+1] = $part . '/' . $s[$i+1];
        }
    }

    return join( ' ', @s );
}

# Determine repository URL
sub get_url {
    my $repo   = shift;
    my $object = shift;
    my @extra  = @_;
    my $u = '';
    my $k;
    my @branch_source;
    my $repo_name;
    my $branch_archive = '';

    defined $repo or die "Unknown source. See `man apt-repo` for details.";
    # Quick forms: known branch name or number for task
    if( exists $branches{ $repo } ) {
        unshift( @extra, $object );
        $object = $repo;
        $repo = 'branch';
    }

    if( $repo =~ /^[0-9]+$/ ) {
        $object = $repo;		
        $repo = 'task';
    }

    # Branch 	
    if( $repo eq 'branch' ) {
        # Branch list
        if ( not defined $object )  {
            # Show all available branch names
            foreach $k (sort keys %branches) {
                print $k . "\n";
            }
            exit 0;
        }

        # URL for branch
        if( exists $branches{ $object } ) {
            my $key = '';
            my $farch = $arch;
            my $altlinuxclub = 0;
            my $autoimports  = 0;
            my $url = $branches{ $object }[0];

            # Check for archive
            $branch_archive = $extra[0] if scalar(@extra) > 0 and defined $extra[0];
            if( $branch_archive ne '' ) {
                die "Repository $object has no archive" if ! grep( /^$object$/, @archiving_branches );
                if( $branch_archive =~ /^\d{8}$/ ) {
                    $branch_archive =~ s/^(\d{4})(\d{2})(\d{2})/$1\/$2\/$3/;
                }
                if( $branch_archive =~ /^\d{4}\/\d{2}\/\d{2}$/ ) {
                    $url = $repo_archive_base . $object . '/date/' . $branch_archive;
                    $new_format_parts = 3;
                } else {
                    die "Archive of repository should be defined as YYYYMMDD or YYYY/MM/DD";
                }
            }

            $altlinuxclub = 1 if $url =~ /^http\:\/\/altlinuxclub\.ru/;
            $autoimports  = 1 if $url =~ /\/ALTLinux\/autoimports\//;

            # Hack $arch for altlinuxclub.ru
            $farch = 'i686' if $altlinuxclub and $arch eq 'i586';

            if( $branches{ $object }[1] ne "" ) {
                $key = '[' . $branches{ $object }[1] . '] ';
            }
            $u = 'rpm ' . $key . $url;

            # Sources list
            @branch_source = ( $u . ' ' . $farch . ' ' . $branches{ $object }[2] );
            push( @branch_source, $u . ' ' . $noarch . ' ' . $branches{ $object }[2] ) if not $altlinuxclub;

            # For x86_64 add Arepo 2.0 source
            if( $use_arepo and $farch eq 'x86_64' and not $altlinuxclub and not $autoimports ) {
                push( @branch_source, $u . ' x86_64-i586 ' . $branches{ $object }[2] );
            }
        } else {
            die "Unknown branch name '$object'\n";
        }
        #print join("\n", @branch_source), "\n";
        return @branch_source;

    }

    # Task
    if( $repo eq 'task' ) {
        if( not defined $object ) {
            die "Task number is missed.\n";
        }
        my @ret = ( 'rpm ' . $repo_task . $object . '/ ' . $arch . ' ' . $c_task );
        if( $use_arepo and $arch eq 'x86_64' and ( $ARGV[0] eq 'rm' or task_has_arepo( $object ) ) ) {
            # Arepo source for x86_64
            push @ret, 'rpm ' . $repo_task . $object . '/ x86_64-i586 ' . $c_task;
        }
        return @ret;
    }

    # URL
    if( $repo =~ /^(http|https|ftp|rsync|file|copy|cdrom):\// ) {
        my $u = 'rpm ' . $repo;
        my $component = $c_branch;
        my @repos = ();
        my $path;

        if( defined $object ) {
            # Architecture is defined
            return ( $u . ' ' . $object . ' ' . join( ' ', @extra ) );
        } else {
            # Mirror
            push( @repos, $u . ' ' . $arch . ' ' . $component );
            if( $repo =~ /^file:\// ) {
                $path = $repo;
                $path =~ s/^file://;
                $path .= '/x86_64-i586';
                push( @repos, $u . ' x86_64-i586  ' . $component ) if -d $path and $use_arepo;
            }
            push( @repos, $u . ' ' . $noarch . ' ' . $component );
            return @repos;
        }
    }

    # Absolute path for hasher repository
    if( $repo =~ /^\// ) {
        return ( 'rpm file://' . $repo . ' ' . $arch . ' hasher' );
    }

    # In format of sources.list(5)
    if( $repo =~ /^$type(-src|-dir)?\b/ ) {
        if( defined $object ) {
            return ( $repo . ' ' . $object . ' ' . join( ' ', @extra ) );
        } else {
            return ( $repo );
        }
    }

    return ();
}

# Add repository to list
sub add_repo {
    shift;
    my $repo   = shift;
    my $object = shift;
    my @comps  = @_;
    my $a_found;
    my $i_found;
    my @c = ();
	my @priority_branches = ( "p10", "p11", "sisyphus" );

    my @urls = get_url( $repo, $object, @comps );

    # Set %_priority_distbranch
	foreach my $distbranch ( @priority_branches ) {
		if( not $dry_run and ( $repo eq $distbranch or ( defined($object) and $object eq $distbranch ) ) ) {
			open PRIORITY, '>', "/etc/rpm/macros.d/priority_distbranch" or die "Can't open /etc/rpm/macros.d/priority_distbranch: $!";
			print PRIORITY "%_priority_distbranch $distbranch\n";
			close PRIORITY;
		}
	}

    if( scalar @urls == 0 ) {
        die "Nothing to add: bad source format. See `man apt-repo` for details.\n";
    }

    foreach my $u ( @urls ) {
        my $unew = new_format( $u );
        $a_found = 0;
        $i_found = 0;

        #print "added $u\n";

        # Lookup active ones
        foreach( get_repos( '-a' ) ) {
            chomp;
            # Check active ones
            if( /^\Q$u\E$/ or /^\Q$unew\E$/ ) {
                # This source is active
                $a_found = 1;
                last;
            }
            # Check commented ones
            if( /^[[:space:]]*#[[:space:]]*\Q$u\E$/ or /^[[:space:]]*#[[:space:]]*\Q$unew\E$/ ) {
                # This source is existing and commented
                $i_found = 1;
                last;
            }
        }

        #print "$a_found $i_found $u\n";

        # Process source
        next if $a_found; # Source is active, nothing do

        if( $i_found ) {
            my $ret_file;
            my $l0;
            my $l;
            # Uncomment commented source
            my $ur = quotemeta $u;
            my $urnew = quotemeta $unew;
            # Iterate through config files
            foreach my $name ( get_source_files() ) {
                open( FILE, "<", $name ); @c = ( <FILE> ); close FILE;
                foreach ( @c ) {
                    if( $dry_run ) {
                        $l = $_;
                        $l0 = $l;
                        $l =~ s/^[[:space:]]*#[[:space:]]*($ur)$/${1}/;
                        $l =~ s/^[[:space:]]*#[[:space:]]*($urnew)$/${1}/;
                        if( $l0 ne $l ) {
                            print "$name:-$l0";
                            print "$name:+$l";
                        }
                    } else {
                        s/^[[:space:]]*#[[:space:]]*($ur)$/${1}/;
                        s/^[[:space:]]*#[[:space:]]*($urnew)$/${1}/;
                    }
                }
                if( ! $dry_run ) {
                    $ret_file = open( FILE, ">", $name );
                    if ( not defined $ret_file ) {
                        die "Unable to edit file $name. Possibly, permission denied. Exiting.\n";
                    }
                    if ( $ret_file != 0 ) {
                        print FILE @c;
                        close FILE;
                    }
                }
            }
            next;
        } else {
            # Append to main sources list file
            if( $dry_run ) {
                print "$conf_main:+$unew\n";
            } else {
                open CONFIG, '>>', "$conf_main" or die "Can't open $conf_main: $!";
                print CONFIG $unew . "\n";
                close CONFIG;
            }
        }
    }

    exit 0 if ! $continues;
}

# Remove disttag macro
sub remove_disttag {
    unlink( "/etc/rpm/macros.d/p10" ) if -e "/etc/rpm/macros.d/p10";
}

# Remove repository from list
sub rm_repo {
    shift;
    my $repo   = shift;
    my $object = shift;
    my @comps  = @_;
    my $a_found;
    my $i_found;
    my @c = ();
    my $branch_source;

    my @urls;

    if( defined $repo and $repo eq 'all' ) {
        # Remove all active sources
        @urls = get_repos();

        # Remove repositories by specified type
        if( defined $object ) {
            $object =~ /^(branch|branches|task|tasks|cdrom|cdroms)$/ or die "Missing repository type for `apt-repo rm all <type>`";
            @urls = grep( /[[:space:]]+classic$/, @urls ) if( $object =~ /^branch(es)?$/ );
            @urls = grep( /[[:space:]]+task$/, @urls ) if( $object =~ /^task(s)?$/ );
            @urls = grep( /^[[:space:]]*rpm[[:space:]]+cdrom:/, @urls ) if( $object =~ /^cdrom(s)?$/ );
        }

        # Remove disttag macro
        remove_disttag();

    } else {
        @urls = get_url( $repo, $object, @comps );
        # Add key [<branch_name>] in addition to [updates] for branch source
        if( exists $branches{ $repo } ) {
            $object = $repo;
            $repo = 'branch';
        }
        if( $repo eq 'branch' ) {
            foreach( @urls ) {
                if( / \[updates\] / ) {
                    $branch_source = $_;
                    $branch_source =~ s/\[updates\]/[\Q$object\E]/;
                    push( @urls, $branch_source );
                }
            }
            # Remove disttag macro
            remove_disttag();
        }
    }

    foreach my $u ( @urls ) {
        my $unew = new_format( $u );
        $a_found = 0;
        $i_found = 0;

        chomp $u;

        # Lookup active
        foreach( get_repos() ) {
            chomp;
            # Check active
            if( /^\Q$u\E$/ or /^\Q$unew\E$/ ) {
                # This source is active
                $a_found = 1;
                last;
            }
        }

        #print "$a_found $u\n";

        # Remove from $conf_main, comment in other files
        if( $a_found ) {
            my $ret_file;
            my @cc;
            my $l0;
            my $l;
            $u = quotemeta $u;
            $unew = quotemeta $unew;
            # Iterate through config files
            open( FILE, "<", $conf_main ); @c = ( <FILE> ); close FILE;
            if( ! $dry_run ) {
                @cc = grep {!/^[[:space:]]*$u$/} @c;
                @cc = grep {!/^[[:space:]]*$unew$/} @cc;
                $ret_file = open( FILE, ">", $conf_main );
                if ( not defined $ret_file ) {
                    die "Unable to edit file $conf_main. Possibly, permission denied. Exiting.\n";
                    return 1;
                }
                if ( $ret_file != 0 ) {
                    print FILE @cc;
                    close FILE;
                }
            } else {
                my @ca = ();
                @cc = grep {/^[[:space:]]*$u$/} @c;
                push( @ca, @cc ) if @cc;
                @cc = grep {/^[[:space:]]*$unew$/} @c;
                push( @ca, @cc ) if @cc;
                foreach( @ca ) {
                    print "$conf_main:-$_";
                }
            }
            # Comment in config files
            foreach my $name ( get_source_files() ) {
                open( FILE, "<", $name ); @c = ( <FILE> ); close FILE;
                foreach ( @c ) {
                    if( $dry_run ) {
                        $l = $_;
                        $l0 = $l;
                        $l =~ s/^[[:space:]]*($u)$/#${1}/;
                        $l =~ s/^[[:space:]]*($unew)$/#${1}/;
                        if( $l0 ne $l and $name ne $conf_main ) {
                            print "$name:-$l0";
                            print "$name:+$l";
                        }
                    } else {
                        s/^[[:space:]]*($u)$/#${1}/;
                        s/^[[:space:]]*($unew)$/#${1}/;
                    }
                }
                if( ! $dry_run ) {
                    open( FILE, ">", $name ) or next; print FILE @c; close FILE;
                }
            }
        }
    }
    return 0;
}

# Remove all cdrom and task repositories
sub clear_repo {
    my @cmd;

    @cmd = qw(rm all cdroms);
    rm_repo( @cmd );

    @cmd = qw(rm all tasks);
    rm_repo( @cmd );

    exit 0 if ! $continues;
}

# Remove all and set new branch
sub set_repo {
    shift;
    my $branch = shift;
    my @cmd;

    @cmd = qw(rm all);
    rm_repo( @cmd );

    @cmd = qw(add branch);
    push( @cmd, $branch );
    add_repo( @cmd );
}

# Update repo
sub update_repo {
    my $error_code = 0;
    if (! $hsh ) {
        $error_code = system 'apt-get update';
    } else {
        # Normally, hsh --initroot-only prepares an environment with rpm-build etc. (for building packages).
        # Instead, here, we do as girar's install check does, which prepares a smaller minimal system:
        # http://git.altlinux.org/people/ldv/packages/?p=girar.git;a=blob;f=gb/remote/gb-remote-check-install;h=e7823af17cdfc68369f5782a8cdbac18e581adb7;hb=1535653ca9923ea8cd228ae68b2ca7c1b80eba7e#l41
        # This would allow us to reproduce the bugs that happen in girar's install check, like that one: https://bugzilla.altlinux.org/show_bug.cgi?id=31576 .
        # TODO:
        # The difference is that girar explicitly knows that target repo branch (like Sisyphus, p7, etc.), and hence is able to use an explicit altlinux-release-sisyphus package.
        # We simply rely on the virtual package. (Which is not very nice, because it can install a different special package.)
        $error_code = system "hsh --apt-config=\"\$APT_CONFIG\" --initroot-only --pkg-init-list=+altlinux-release --pkg-build-list=altlinux-release,basesystem";
    }
    if ( $error_code ) {
        warn 'Error: The "update" command has not completed successfully';
    }
    exit $error_code if ! $continues;
    return !$error_code;
}

# Test task
sub test_task {
    shift;
    my $task = shift;
    my @pkgs = ();
    my $list = "";

    $task = shift if $task eq 'task';

    @pkgs = @_;
    if ( scalar(@pkgs) == 0 ) {
        @pkgs = get_task_content( $task );
    }

    # Add source and update indices
    $continues = 1;
    if ( not $task eq '' ) {
        add_repo( ("add", "task", $task ) );
    }
    if ( update_repo() or ! $hsh ) {
        # `hsh --initroot-only` is particularly fragile,
        # so if we are using hsh and if the "update" command
        # hasn't completed successfully, we abort.

        # Install all packages from task (except *-debuginfo)
        chomp( @pkgs );
        $list = join( " ", @pkgs );

        # Install packages from task repository
        if (! $hsh ) {
            system "cd /var/empty/; apt-get install $list";
        } else {
            system "cd /var/empty/; hsh-install $list";
        }

    }

    # Remove task source
    if ( not $task eq '' ) {
        rm_repo( ("rm", "task", $task ) );
    }

    exit 0;
}

# Upgrade system including optional task repos
sub upgrade {
    shift;
    my @tasks = @_;
    my $error_code = 0;

    $continues = 1;

    # Add extra task repos
    foreach my $task ( @tasks ) {
        add_repo( ("add", "task", $task ) );
    }

    # Update indices
    update_repo();

    # Perform full upgrade
    $error_code = system 'apt-get dist-upgrade';

    # Remove extra task repos
    foreach my $task ( @tasks ) {
        rm_repo( ("rm", "task", $task ) );
    }

    exit $error_code;
}

# Process command line arguments
# Exiting functions
show_repo( @ARGV ) if $cmd eq 'list';
show_repo( 'list', '-a' ) if $cmd eq '-a';
show_usage() if $cmd =~ /-(h|-help)$/ or scalar @ARGV == 0;
show_version() if $cmd =~ /-(v|-version)$/;
clear_repo( @ARGV ) if $cmd =~ /^clea[rn]$/;
set_repo( @ARGV ) if $cmd eq 'set';
update_repo() if $cmd eq 'update';
test_task( @ARGV ) if $cmd eq 'test';
upgrade( @ARGV ) if $cmd eq 'upgrade';

# Functions with return
if( $cmd =~ /^(add|rm)$/ ) {
    add_repo( @ARGV ) if $cmd eq 'add';
    rm_repo( @ARGV ) if $cmd eq 'rm';
} else {
    die "Unknown command `$cmd`.\nRun `apt-repo --help` for supported commands.\n";
}

__END__
