NAME

CPAN::InGit - Manage custom CPAN trees to pin versions for your projects

SYNOPSIS

Using the module:

my $git_repo= Git::Raw::Repository->discover($repo_path // getcwd);
my $cpan_repo= CPAN::InGit->new(git_repo => $git_repo);

# Create a mirror of public CPAN
# Setting "upstream_url" creates a partial mirror which advertises all
# current public CPAN versions of modules, and fetches dists on demand
# and commits them to that branch, as a cache.
$cpan_repo->create_archive_tree('www_cpan_org', upstream_url => 'https://www.cpan.org');

# Create a branch to be the per-application tree of modules.  Configure
# it to "import_modules" from branch "www_cpan_org".
$cpan_repo->create_archive_tree('my_app',
  default_import_sources => ['www_cpan_org'],
  corelist_perl_version => '5.026003',
);

# This pulls modules Catalyst and DBIx::Class from the www_cpan_org branch
# (which fetches and commits on demand) and then adds them to the package
# index of branch 'my_app'.
my $app_pan= $cpan_repo->get_archive_tree('my_app');
$app_pan->import_modules({
  'Catalyst' => 0,
  'DBIx::Class' => 0,
});

# Commit the changes to branch 'my_app'
$app_pan->commit("Added Catalyst and DBIx::Class");

# This only pulls Log::Any, because the versions of Catalyst and DBIx::Class
# are already satisfied, even if new versions of DBIx::Class were available,
# and even if those new versions were in branch 'www_cpan_org'.
# The versions are pinned until you request a newer version.
$app_pan->import_modules({
  'Catalyst' => 0,
  'DBIx::Class' => 0,
  'Log::Any' => 1,
});

Using the command line to do the same as above:

mkdir localpan && cd localpan && git init
cpangit-create --upstream_url=https://www.cpan.org www_cpan_org
cpangit-create --from=www_cpan_org --corelist=v5.26 my_app
cpangit-add --branch=my_app Catalyst DBIx::Class
cpangit-add --branch=my_app Catalyst DBIx::Class Log::Any
cpangit-server -l http://localhost:3000 &
cpanm -M http://localhost:3000/my_app/ Catalyst DBIx::Class

DESCRIPTION

WARNING This module is in an early state of development, and the API is not final.

CPAN::InGit is a concept that instead of using Carton and a cpanfile.spanshot to request an exact list of modules for your project, you store the exact list in a Git repo and then serve that as a CPAN mirror to your CPAN client as if these are the only versions of modules that exist. You can then use any CPAN client you like without fussing with where it will install the modules or how it will decide when to upgrade versions of dependencies.

Eventually, I plan to have functionality to help "curate" a collection of CPAN modules, such as applying patches for known issues in modules, ensuring application branches upgrade to those patched versions, upgrading back to the public CPAN verison when the fix gets picked up by the primary author, or identifying when dependencies have CVEs that require an upgrade.

Features

  • It's your own private CPAN (DarkPAN) with all the benefits that entails. (such as private distributions or patching public distributions)

  • By hosting CPAN modules on your own infrastructure, you can avoid hammering public CPAN with your CI/CD builds every time you push a commit.

  • The data is stored in Git, so it's version controlled and compressed. You can revert to a previous environment for your application with a simple "git revert".

  • The server serves each branch as its own mirror URL, so it's actually like an unlimited number of DarkPANs hosted on the same server. You can reference the server sub-path with your project's Dockerfile like cpanm -M http://ingit-server.local/my_app_branch/.

  • This module reads directly from the Git repo storage without needing a checkout. You can point the server at the same repository being hosted by Gitea.

  • Removes the need for cpanfile.snapshot or Carton, because now the DarkPAN mirror is versioning your environment for each application.

ATTRIBUTES

git_repo

An instance of Git::Raw::Repository (which wraps libgit2.so) for accessing the git structures. You can pass this attribute to the constructor as a simple directory path which gets inflated to a Repository object.

git_author_name

Name used for commits generated by this library. Defaults to 'CPAN::InGit'

git_author_email

Email used for commits generated by this library. Defaults to 'CPAN::InGit@localhost'

useragent

The Mojo::UserAgent object used when downloading files from the real CPAN.

METHODS

get_archive_tree

$mirror= $cpan_repo->get_archive_tree($branch_or_tag_or_id);

Return a ArchiveTree object for the given branch name, git tag, or commit hash. This branch must look like an archive (having /cpan_ingit.json) or it will return undef.

create_archive_tree

$mirror= $cpan_repo->create_archive_tree($branch_name, %params);

Create a new mirror branch. The branch must not already exist.

lookup_tree

$tree= $cpan_repo->lookup_tree($branch_or_tag_or_commit);
($tree, $origin)= $cpan_repo->lookup_tree($branch_or_tag_or_commit);

Return the Git::Raw::Tree object for the given branch name, git tag, or commit hash. Returns undef if not found. In list context, it returns both the tree and the origin object (commit, branch, or tag) for that tree.

add_git_tree_to_tar

$mirrorInGit->add_git_tree_to_tar($tar, $path, $tree);

This utility function adds Git trees to a tar archve, calling "add_git_dirent_to_tar" for each entry. $path provides the name for the root of the tree within the archive. undef or empty string means the tree will be the root of the archive.

add_git_dirent_to_tar

$mirrorInGit->add_git_dirent_to_tar($tar, $path, $dirent);

This utility function adds Git directory entries to a tar archve. It recurses subdirectories and handles symlinks. The $path is used for the destination name instead of $dirent->name.

new_signature

Returns a Git::Raw::Signature that will be used for commits authored by this module. Signatures contain a timestamp, so the library generates new signatures frequently during operation.

lookup_versions

$version_list= $cpan_repo->lookup_versions($module_name);

This returns a list of all versions of that module which are already cached in any Mirror of this repo, and also any version which is available from any of the upstream mirrors listed in "upstream_mirrors".

process_distfile

my $index= $darkpan->process_distfile(
  tree      => $mirror_tree,
  file_path => $path,
  file_data => \$bytes,
  untar     => $bool,   # whether to extract tar file into the tree
);

VERSION

version 0.002

AUTHOR

Michael Conrad <mike@nrdvana.net>

COPYRIGHT AND LICENSE

This software is copyright (c) 2025 by Michael Conrad, and IntelliTree Solutions.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.