Source code for swh.loader.git.from_disk

# Copyright (C) 2015-2026  The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU General Public License version 3, or any later version
# See top-level LICENSE file for more information

import logging
import os
import tempfile
from typing import Callable, Dict

from dulwich.object_format import DEFAULT_OBJECT_FORMAT
import dulwich.objects
from dulwich.pack import generate_unpacked_objects, write_pack_data
import dulwich.repo

from swh.loader.git import utils
from swh.loader.git.loader import FetchPackReturn, GitLoader, RepoRepresentation

logger = logging.getLogger(__name__)


[docs] class GitLoaderFromArchive(GitLoader): """Load a git repository from an archive. This loader ingests a git repository compressed into an archive. The supported archive formats are ``.zip`` and ``.tar.gz``. It notably supports the loading of a repository with missing objects that can be obtained by using the filter option from the git clone command (requires git server to have such feature implemented), for instance:: # clone a repository without fetching blobs not reachable from HEAD $ git clone <repo_url> --filter=blob:none # clone a repository without fetching trees and blobs not reachable from HEAD $ git clone <repo_url> --filter=tree:0 It can be useful to load such repositories when writing tests that do not need to process blob and tree objects so we can produce an archive of smaller size. From an input tarball named ``my-git-repo.zip``, the following layout is expected in it:: my-git-repo/ ├── .git │ ├── branches │ ├── COMMIT_EDITMSG │ ├── config │ ├── description │ ├── HEAD ... Nevertheless, the loader is able to ingest tarballs with the following layouts too:: . ├── .git │ ├── branches │ ├── COMMIT_EDITMSG │ ├── config │ ├── description │ ├── HEAD ... or:: other-repo-name/ ├── .git │ ├── branches │ ├── COMMIT_EDITMSG │ ├── config │ ├── description │ ├── HEAD ... """ def __init__(self, *args, archive_path: str, **kwargs): super().__init__(*args, **kwargs) self.archive_path = archive_path
[docs] def project_name_from_archive(self, archive_path): """Compute the project name from the archive's path.""" archive_name = os.path.basename(archive_path) for ext in (".zip", ".tar.gz", ".tgz"): if archive_name.lower().endswith(ext): archive_name = archive_name[: -len(ext)] break return archive_name
[docs] def fetch_pack_from_origin( self, origin_url: str, base_repo: RepoRepresentation, do_activity: Callable[[bytes], None], credentials: Dict[str, str] | None = None, ): with tempfile.TemporaryDirectory() as tmp_dir: project_name = self.project_name_from_archive(self.archive_path) _, repo_path = utils.init_git_repo_from_archive( project_name, self.archive_path, root_temp_dir=tmp_dir ) pack_buffer = tempfile.SpooledTemporaryFile(max_size=self.temp_file_cutoff) with dulwich.repo.Repo(repo_path) as repo: # create a pack file with all objects referenced in the repository so # we can load it even if it has missing objects unpacked_objects = generate_unpacked_objects( repo.object_store, list(map(lambda sha: (sha, None), repo.object_store)), ) write_pack_data( pack_buffer.write, records=unpacked_objects, object_format=DEFAULT_OBJECT_FORMAT, num_records=sum(1 for _ in repo.object_store), ) pack_buffer.flush() pack_size = pack_buffer.tell() pack_buffer.seek(0) return FetchPackReturn( remote_refs=utils.filter_refs(repo.refs.as_dict()), symbolic_refs=utils.filter_symbolic_refs(repo.refs.get_symrefs()), pack_buffer=pack_buffer, pack_size=pack_size, )