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