-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathupdate_patch.py
More file actions
183 lines (149 loc) · 6.87 KB
/
update_patch.py
File metadata and controls
183 lines (149 loc) · 6.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
"""Updating patches.
*Dfetch* allows you to keep local changes to external projects in the form of
patch files. When those local changes evolve over time, an existing patch can
be updated to reflect the new state of the project.
The ``update-patch`` command automates the otherwise manual process of
refreshing a patch. It safely regenerates the last patch of a project based on
the current working tree, while keeping the upstream revision unchanged.
This command operates on projects defined in the :ref:`Manifest` and requires
that the manifest itself is located inside a version-controlled repository
(the *superproject*). The version control system of the superproject is used to
calculate and regenerate the patch.
The below statement will update the patch for ``some-project`` from your manifest.
.. code-block:: console
$ dfetch update-patch some-project
.. tabs::
.. tab:: Git
.. scenario-include:: ../features/update-patch-in-git.feature
.. tab:: SVN
.. scenario-include:: ../features/update-patch-in-svn.feature
"""
import argparse
import pathlib
import dfetch.commands.command
import dfetch.manifest.project
import dfetch.project
from dfetch.log import get_logger
from dfetch.project import create_super_project
from dfetch.project.gitsuperproject import GitSuperProject
from dfetch.project.metadata import Metadata
from dfetch.project.superproject import NoVcsSuperProject, RevisionRange
from dfetch.util.util import (
catch_runtime_exceptions,
check_no_path_traversal,
in_directory,
)
logger = get_logger(__name__)
class UpdatePatch(dfetch.commands.command.Command):
"""Update a patch to reflect the last changes.
The ``update-patch`` command regenerates the last patch of one or
more projects based on the current working tree. This is useful
when you have modified a project after applying a patch and want
to record those changes in an updated patch file. If there is no
patch yet, use ``dfetch diff`` instead.
"""
@staticmethod
def create_menu(subparsers: dfetch.commands.command.SubparserActionType) -> None:
"""Add the menu for the update-patch action."""
parser = dfetch.commands.command.Command.parser(subparsers, UpdatePatch)
parser.add_argument(
"projects",
metavar="<project>",
type=str,
nargs="*",
help="Specific project(s) to update",
)
def __call__(self, args: argparse.Namespace) -> None:
"""Perform the update patch."""
superproject = create_super_project()
exceptions: list[str] = []
if isinstance(superproject, NoVcsSuperProject):
raise RuntimeError(
"The project containing the manifest is not under version control,"
" updating patches is not supported"
)
if not isinstance(superproject, GitSuperProject):
logger.warning("Update patch is only fully supported in git superprojects!")
with in_directory(superproject.root_directory):
for project in superproject.manifest.selected_projects(args.projects):
with catch_runtime_exceptions(exceptions) as exceptions:
subproject = dfetch.project.create_sub_project(project)
destination = project.destination
def _ignored(dst: str = destination) -> list[str]:
return list(superproject.ignored_files(dst))
# Check if the project has a patch, maybe suggest creating one?
if not subproject.patch:
logger.print_warning_line(
project.name,
f'skipped - there is no patch file, use "dfetch diff {project.name}"'
" to generate one instead",
)
continue
# Check if the project was ever fetched
on_disk_version = subproject.on_disk_version()
if not on_disk_version:
logger.print_warning_line(
project.name,
f'skipped - the project was never fetched before, use "dfetch update {project.name}"',
)
continue
# Make sure no uncommitted changes (don't care about ignored files)
if superproject.has_local_changes_in_dir(subproject.local_path):
logger.print_warning_line(
project.name,
f"skipped - Uncommitted changes in {subproject.local_path}",
)
continue
# force update to fetched version from metadata without applying patch
subproject.update(
force=True,
ignored_files_callback=_ignored,
patch_count=len(subproject.patch) - 1,
)
# generate reverse patch
patch_text = superproject.diff(
subproject.local_path,
revisions=RevisionRange("", ""),
ignore=(Metadata.FILENAME,),
reverse=True,
)
# Select patch to overwrite & make backup
if not self._update_patch(
subproject.patch[-1],
superproject.root_directory,
project.name,
patch_text,
):
continue
# force update again to fetched version from metadata but with applying patch
subproject.update(
force=True, ignored_files_callback=_ignored, patch_count=-1
)
if exceptions:
raise RuntimeError("\n".join(exceptions))
def _update_patch(
self,
patch_to_update: str,
root: pathlib.Path,
project_name: str,
patch_text: str,
) -> pathlib.Path | None:
"""Update the specified patch file with new patch text."""
patch_path = pathlib.Path(patch_to_update).resolve()
try:
check_no_path_traversal(patch_path, root)
except RuntimeError:
logger.print_warning_line(
project_name,
f'No updating patch "{patch_to_update}" which is outside {root}',
)
return None
if patch_text:
logger.print_info_line(project_name, f'Updating patch "{patch_to_update}"')
patch_path.write_text(patch_text, encoding="UTF-8")
else:
logger.print_info_line(
project_name,
f"No diffs found, kept patch {patch_to_update} unchanged",
)
return patch_path