lib/ES/Repo.pm (294 lines of code) (raw):

package ES::Repo; use strict; use warnings; use v5.10; use Cwd; use Encode qw(decode_utf8); use ES::Util qw(run); use Path::Class(); use parent qw( ES::BaseRepo ); my %Repos; #=================================== sub new { #=================================== my ( $class, %args ) = @_; my $self = $class->SUPER::new(%args); my $name = $self->name; $self->{tracker} = $args{tracker} or die "No <tracker> specified for repo <$name>"; $self->{keep_hash} = $args{keep_hash}; $Repos{$self->name} = $self; $self; } #=================================== sub get_repo { #=================================== my $class = shift; my $name = shift || ''; return $Repos{$name} || die "Unknown repo name <$name>"; } #=================================== sub add_sub_dir { #=================================== my ( $self, $branch, $dir ) = @_; $self->{sub_dirs}->{$branch} = $dir; return (); } #=================================== # Returns 'not_changed' if the repo hasn't changed since we last built it, # 'changed' if it has, and 'new_sub_dir' if this is a sub_dir for a new source. #=================================== sub has_changed { #=================================== my $self = shift; my ( $title, $branch, $path ) = @_; $path = $self->normalize_path( $path, $branch ); $branch = $self->normalize_branch( $branch ); local $ENV{GIT_DIR} = $self->git_dir; my $old_info = $self->_last_commit_info(@_); my $new; if ( $self->{keep_hash} ) { # If we're keeping the hash from the last build but there *isn't* a # hash that means that the branch wasn't used the last time we built # this book. That means we'll skip it entirely when building the book # anyway so we should consider the book not to have changed. unless ($old_info) { # New sub_dirs *might* build, but only if the entire book is built # out of new sub_dirs. return 'new_sub_dir' if exists $self->{sub_dirs}->{$branch}; return 'not_changed'; } $new = $self->_last_commit(@_); } else { # If we aren't keeping the hash from the last build and there *isn't* # a hash that means that this is a new repo so we should build it. return 'changed' unless $old_info; $new = $self->sha_for_branch( $branch ) or die( "Remote branch <origin/$branch> doesn't exist in repo " . $self->name); } my $new_info = $new; # We check sub_dirs *after* the checks above so we can handle sub_dir for # new sources specially. return 'changed' if exists $self->{sub_dirs}->{$branch}; if ($self->{keep_hash}) { return $old_info ne $new_info ? 'changed' : 'not_changed'; } return 'not_changed' if $old_info eq $new_info; my $changed; eval { $changed = !!run qw(git diff --shortstat), $self->_last_commit(@_), $new, '--', $path; 1; } || do { $changed = 1 }; return $changed ? 'changed' : 'not_changed'; } #=================================== sub mark_done { #=================================== my $self = shift; my ( $title, $branch, $path ) = @_; my $new; if ( exists $self->{sub_dirs}->{$branch} ) { $new = 'local'; } elsif ( $self->{keep_hash} ) { $new = $self->_last_commit($title, $branch, $path); return unless $new; # Skipped if nil } else { $new = $self->sha_for_branch( $branch ); } $self->tracker->set_sha_for_branch( $self->name, $self->_tracker_branch(@_), $new ); } #=================================== sub prepare { #=================================== my $self = shift; my ( $title, $branch, $path, $dest_root, $prefix ) = @_; my $dest = $dest_root->subdir( $prefix ); if ( exists $self->{sub_dirs}->{$branch} ) { $self->_prepare_sub_dir( $title, $branch, $path, $dest ); return $dest; } my $resolved_branch = $self->_resolve_branch( @_ ); unless ( $resolved_branch ) { printf(" - %40.40s: Skipping new repo %s for branch %s.\n", $title, $self->{name}, $branch); return $dest; } $self->_extract_from_ref( $dest, $branch, $path ); return $dest; } #=================================== sub _prepare_sub_dir { #=================================== my ( $self, $title, $branch, $path, $dest ) = @_; my $source_root = $self->{sub_dirs}->{$branch}; unless ( $self->{keep_hash} ) { $self->_extract_from_dir( $source_root, $dest, $path ); return $dest; } my $no_uncommitted_changes = eval { local $ENV{GIT_WORK_TREE} = $source_root; local $ENV{GIT_DIR} = $ENV{GIT_WORK_TREE} . '/.git'; run qw(git diff-index --quiet HEAD --); 1; }; unless ( $no_uncommitted_changes ) { unless ( $@ =~ /\n---out---\n\n---err---\n\n---------\n/) { # If the error message isn't empty then something went wrong checking. die "failed to check for outstanding commits: $@"; } printf(" - %40.40s: Not merging the subbed dir for [%s][%s][%s] because it has uncommitted changes.\n", $title, $self->{name}, $branch, $path); $self->_extract_from_dir( $source_root, $dest, $path ); return $dest; } my $resolved_branch = $self->_resolve_branch( $title, $branch, $path ); unless ( $resolved_branch ) { printf(" - %40.40s: Not merging the subbed dir for [%s][%s][%s] because it is new.\n", $title, $self->{name}, $branch, $path); $self->_extract_from_dir( $source_root, $dest, $path ); return $dest; } local $ENV{GIT_DIR} = "$source_root/.git"; my $subbed_head = run qw(git rev-parse HEAD); delete local $ENV{GIT_DIR}; my $merge_branch = "${resolved_branch}_${subbed_head}_$path"; $merge_branch =~ s|/|_|g; $merge_branch =~ s|\*|splat|g; $merge_branch =~ s|:\(glob\)|_glob_|g; $merge_branch =~ s|\.$||g; # Fix funny shaped paths # Check if we've already merged this path by looking for the merged_branch # in the source repo. This is safe because: # 1. We prune all branches from the source repo before the build. # 2. The merge branch contains the hash of the subbed head. my $already_built = eval { local $ENV{GIT_DIR} = $self->{git_dir}; run qw(git rev-parse), $merge_branch; 1; }; if ( $already_built ) { # Logging here would be pretty noisy. $self->_extract_from_ref( $dest, $merge_branch, $path ); return; } # Merge the HEAD of the subbed dir into the commit that last successfully # built the docs. printf(" - %40.40s: Merging the subbed dir for [%s][%s][%s] into the last successful build.\n", $title, $self->{name}, $branch, $path); my $work = Path::Class::dir( '/tmp/mergework' ); $work->rmtree; run qw(git clone --no-checkout), $self->{git_dir}, $work; my $original_pwd = Cwd::cwd(); chdir $work; my $merged = eval { run qw(git remote add subbed_repo), $source_root; run qw(git fetch subbed_repo), $subbed_head; run qw(git remote remove subbed_repo); unless ($path eq '.') { run qw(git config core.sparseCheckout true); $self->_write_sparse_config( $work, $path ); } run qw(git checkout -b), $merge_branch, $resolved_branch; run qw(git merge -m merge), $subbed_head; run qw(git push origin -f), $merge_branch; # Origin here is just our clone. 1; }; chdir $original_pwd; unless ( $merged ) { printf(" - %40.40s: Failed to merge the subbed dir for [%s][%s][%s] into the last successful build:\n%s", $title, $self->{name}, $branch, $path, $@); $dest->rmtree; $self->_extract_from_dir( $source_root, $dest, $path ); return $dest; } printf(" - %40.40s: Merged the subbed dir for [%s][%s][%s] into the last successful build.\n", $title, $self->{name}, $branch, $path); $self->_extract_from_ref( $dest, $merge_branch, $path ); } #=================================== sub _extract_from_dir { #=================================== my ( $self, $source_root, $dest_root, $path ) = @_; local $ENV{GIT_WORK_TREE} = $source_root; local $ENV{GIT_DIR} = $ENV{GIT_WORK_TREE} . '/.git'; # Copies the $path from the subsitution directory. It is tempting to # just symlink the substitution directory into the destination and # call it a day but that doesn't work for a whole bunch of non-obvious # reasons: # 1. We often use relative paths to include asciidoc files from other # repositories and those relative paths don't work at all with symlinks. # 2. The paths themselves are actually git pathspecs which can resolve to # more than one file. my $files = run qw(git ls-files -zcom --), $path; for ( split /\0/, $files ) { my $source = $source_root->file( $_ ); die "Can't find $source" unless -e $source; my $dest = $dest_root->file( $_ ); $dest->parent->mkpath; $source->copy_to( $dest ) or die "Error copying from $source: $@"; } } #=================================== sub _extract_from_ref { #=================================== my ( $self, $dest, $ref, $path ) = @_; local $ENV{GIT_DIR} = $self->{git_dir}; $dest->mkpath; my $tar = $dest->file( '.temp_git_archive.tar' ); die "File <$tar> already exists" if -e $tar; run qw(git archive --format=tar -o), $tar, $ref, $path; run qw(tar -x -C), $dest, '-f', $tar; $tar->remove; } #=================================== sub _tracker_branch { #=================================== my $self = shift; my $title = shift or die "No <title> specified"; my $branch = shift or die "No <branch> specified"; my $path = shift or die "No <path> specified"; $path = $self->normalize_path( $path, $branch ); return "$title/${path}/${branch}"; } #=================================== sub edit_url { #=================================== my ( $self, $branch ) = @_; $branch = $self->normalize_branch( $branch ); return edit_url_for_url_and_branch($self->url, $branch); } #=================================== sub edit_url_for_url_and_branch { #=================================== my ( $url, $branch ) = @_; # If the url is in ssh form, then convert it to https $url =~ s/git@([^:]+):/https:\/\/$1\//; # Strip trailing .git as it isn't in the edit link $url =~ s/\.git$//; my $dir = Path::Class::dir( "edit", $branch )->cleanup->as_foreign('Unix'); return "$url/$dir/"; } #=================================== sub dump_recent_commits { #=================================== my ( $self, $title, $branch, $src_path ) = @_; $branch = $self->normalize_branch( $branch ); $src_path = $self->normalize_path( $src_path, $branch ); my $description = $self->name . "/$title:$branch:$src_path"; if ( exists $self->{sub_dirs}->{$branch} ) { return "Used " . $self->{sub_dirs}->{$branch} . " for $description\n"; } local $ENV{GIT_DIR} = $self->git_dir; my $start = $self->_last_commit( $title, $branch, $src_path ); my $rev_range = $self->{keep_hash} ? $start : "$start...$branch"; my $commits = eval { run( 'git', 'log', $rev_range, '--pretty=format:%h -%d %s (%cr) <%an>', '-n', 10, '--abbrev-commit', '--date=relative', '--', $src_path ); } || ''; unless ( $commits =~ /\S/ ) { $commits = run( 'git', 'log', $branch, '--pretty=format:%h -%d %s (%cr) <%an>', '-n', 10, '--abbrev-commit', '--date=relative', '--', $src_path ); } my $header = "Recent commits in $description"; return $header . "\n" . ( '-' x length($header) ) . "\n" . $commits . "\n\n"; } #=================================== sub show_file { #=================================== my $self = shift; my ( $reason, $branch, $file ) = @_; if ( exists $self->{sub_dirs}->{$branch} ) { my $realpath = $self->{sub_dirs}->{$branch}->file($file); return $realpath->slurp( iomode => "<:encoding(UTF-8)" ); } my $resolved_branch = $self->_resolve_branch( @_ ); die "Can't resolve $branch" unless $resolved_branch; local $ENV{GIT_DIR} = $self->git_dir; return decode_utf8 run( qw (git show ), $resolved_branch . ':' . $file ); } #=================================== # Information about the last commit, *not* including flag. # NOTE: We don't presently need any flags but we probably will in the future. #=================================== sub _last_commit { #=================================== my $self = shift; my $sha = $self->_last_commit_info(@_); $sha =~ s/\|.+$//; # Strip flags (|whatever) if it is in the hash return $sha; } #=================================== # Information about the last commit, including flags. # NOTE: We don't presently need any flags but we probably will in the future. #=================================== sub _last_commit_info { #=================================== my $self = shift; my $tracker_branch = $self->_tracker_branch(@_); my $sha = $self->tracker->sha_for_branch($self->name, $tracker_branch); return $sha; } #=================================== # Converts a branch specification into the branch to actually use in the git # repo. Returns falsy if we've been instructed to keep the hash used by the # last build but we have yet to use the branch. #=================================== sub _resolve_branch { #=================================== my $self = shift; my ( $title, $branch, $path ) = @_; return $branch unless $self->{keep_hash}; $branch = $self->_last_commit(@_); die "--keep_hash can't build on top of --sub_dir" if $branch eq 'local'; # If the build fails, this message can indicate that there are duplicate entries in the conf.yaml return $branch; } #=================================== sub add_source { #=================================== my ( $self, $sources, $prefix, $path, $exclude, $map_branches, $private, $alternatives ) = @_; push @$sources, { repo => $self, prefix => $prefix, path => $path, exclude => $exclude, map_branches => $map_branches, private => $private, alternatives => $alternatives, }; } #=================================== sub tracker { shift->{tracker} } #=================================== 1