Coverage for gsb/_make_zip_archive.py: 83%
29 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-08 20:16 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-08 20:16 +0000
1"""ZIP-compatible `repo.write_archive`, stand-alone for easier upstream sharing.
2As it is adapted directly from pygit2, this module is licensed under the
3GNU Public License v2."""
5import datetime as dt
6import zipfile
7from time import time
9from pygit2 import GIT_FILEMODE_LINK, Commit, Index, Oid, Repository, Tree
12def write_zip_archive(
13 repo: Repository,
14 treeish,
15 archive: zipfile.ZipFile,
16 timestamp: int | None = None,
17 prefix: str = "",
18) -> None:
19 """Implementation of `repo.write_archive` that supports ZIP. Writes treeish
20 into an archive.
22 If no timestamp is provided and 'treeish' is a commit, its committer
23 timestamp will be used. Otherwise the current time will be used.
25 All path names in the archive are added to 'prefix', which defaults to
26 an empty string.
28 Parameters
29 ----------
30 repo: Repository
31 The git repository
32 treeish
33 The treeish to write
34 archive : zipfile.ZipFile
35 An archive from the 'zipfile' module.
36 timestamp : int, optional
37 (Epoch) timestamp to use for the files in the archive.
38 prefix : str, optional
39 Extra prefix to add to the path names in the archive.
41 Notes
42 -----
43 h/t to https://stackoverflow.com/a/18432983 for the example on converting
44 between `TarInfo` and `ZipInfo`.
46 Example
47 -------
48 >>> import tarfile, pygit2
49 >>> from gsb import _git
50 >>> repo = pygit2.Repository('.')
51 >>> with zipfile.ZipFile('foo.zip', 'w') as archive:
52 ... _git.write_zip_archive(repo, repo.head.target, archive)
53 """
54 # Try to get a tree form whatever we got
55 # Try to get a tree form whatever we got
56 if isinstance(treeish, (str, Oid)):
57 treeish = repo[treeish]
59 tree = treeish.peel(Tree)
61 # if we don't have a timestamp, try to get it from a commit
62 if not timestamp:
63 try:
64 commit = treeish.peel(Commit)
65 timestamp = commit.committer.time
66 except Exception:
67 pass
69 # as a last resort, use the current timestamp
70 if not timestamp:
71 timestamp = int(time())
73 datetime = dt.datetime.fromtimestamp(timestamp)
74 zip_datetime = (
75 # per https://docs.python.org/3/library/zipfile.html#zipfile.ZipInfo.date_time
76 datetime.year,
77 datetime.month,
78 datetime.day,
79 datetime.hour,
80 datetime.minute,
81 datetime.second,
82 )
84 index = Index()
85 index.read_tree(tree)
87 for entry in index:
88 content = repo[entry.id].read_raw()
89 info = zipfile.ZipInfo(prefix + entry.path)
90 info.file_size = len(content)
91 info.date_time = zip_datetime
92 # info.uname = info.gname = "root" # git's archive-zip.c does not
93 if entry.mode == GIT_FILEMODE_LINK:
94 raise NotImplementedError(
95 "ZIP archives with symlinks are not currently supported."
96 "\nSee: https://bugs.python.org/issue37921"
97 )
98 # per https://github.com/git/git/blob/3a06386e/archive-zip.c#L339
99 info.external_attr = entry.mode << 16 if (entry.mode & 111) else 0
100 archive.writestr(info, content)