[Diff] Changed repo protection to worktree mechanism

- As suggested by @matthijskooijman on #443
- This is cleaner than trying to use stash push/pop
- The old method is available
This commit is contained in:
Salvador E. Tropea 2023-06-06 12:53:19 -03:00
parent 0e7829616c
commit 7a7beff556
6 changed files with 108 additions and 45 deletions

View File

@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `field_power` Name/s of the field/s used for the power raiting - `field_power` Name/s of the field/s used for the power raiting
- `invalidate_pcb_text_cache` controls if we reset the text variables cached - `invalidate_pcb_text_cache` controls if we reset the text variables cached
in the PCB file. in the PCB file.
- `git_diff_strategy` selects how we preserve the current repo state.
(See #443)
- Filters: - Filters:
- New `value_split` to extract information from the Value field and put it in - New `value_split` to extract information from the Value field and put it in
separated fields. I.e. tolerance, voltage, etc. separated fields. I.e. tolerance, voltage, etc.
@ -53,11 +55,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Command line: - Command line:
- `--list` also lists groups - `--list` also lists groups
- KiCad v6/7 schematic: - KiCad v6/7 schematic:
- The hierarchy is expanded only if needed, i.e. value of an instance changed - When saving an schematic the hierarchy is expanded only if needed,
i.e. value of an instance changed
- List actions: - List actions:
- Now you must explicitly ask to configure outputs. Otherwise isn't needed. - Now you must explicitly ask to configure outputs. Otherwise isn't needed.
As a result you no longer need to have an SCH/PCB. Use `--config-outs` to As a result you no longer need to have an SCH/PCB. Use `--config-outs` to
get the old behavior. get the old behavior.
- Git diff link file name:
- Now we default to using worktrees instead of stash push/pop. As a side
effect the names of the git points are chnaged. This is because main/master
only applies to the main worktree. So the names now refer to the closest
tag.
### Fixed ### Fixed
- KiCad v6/7 schematic: - KiCad v6/7 schematic:

View File

@ -840,6 +840,10 @@ global:
- `number`: [number=0] Error number we want to exclude. - `number`: [number=0] Error number we want to exclude.
- `regex`: [string=''] Regular expression to match the text for the error we want to exclude. - `regex`: [string=''] Regular expression to match the text for the error we want to exclude.
- *regexp*: Alias for regex. - *regexp*: Alias for regex.
- `git_diff_strategy`: [string='worktree'] [worktree,stash] When computing a PCB/SCH diff it configures how do we preserve the current
working state. The *worktree* mechanism creates a separated worktree, that then is just removed.
The *stash* mechanism uses *git stash push/pop* to save the current changes. Using *worktree*
is the preferred mechanism.
- `hide_excluded`: [boolean=false] Default value for the `hide_excluded` option of various PCB outputs. - `hide_excluded`: [boolean=false] Default value for the `hide_excluded` option of various PCB outputs.
- `impedance_controlled`: [boolean=false] The PCB needs specific dielectric characteristics. - `impedance_controlled`: [boolean=false] The PCB needs specific dielectric characteristics.
KiCad 6: you should set this in the Board Setup -> Physical Stackup. KiCad 6: you should set this in the Board Setup -> Physical Stackup.

View File

@ -343,6 +343,11 @@ class Globals(FiltersOptions):
variables update when using `set_text_variables`. You might want to disable it when applying some variables update when using `set_text_variables`. You might want to disable it when applying some
changes to the PCB and create a new copy to send to somebody without changing the cached values. changes to the PCB and create a new copy to send to somebody without changing the cached values.
The `auto` value will remove the cached values only when using `set_text_variables` """ The `auto` value will remove the cached values only when using `set_text_variables` """
self.git_diff_strategy = 'worktree'
""" [worktree,stash] When computing a PCB/SCH diff it configures how do we preserve the current
working state. The *worktree* mechanism creates a separated worktree, that then is just removed.
The *stash* mechanism uses *git stash push/pop* to save the current changes. Using *worktree*
is the preferred mechanism """
self.set_doc('filters', " [list(dict)] KiBot warnings to be ignored ") self.set_doc('filters', " [list(dict)] KiBot warnings to be ignored ")
self._filter_what = 'KiBot warnings' self._filter_what = 'KiBot warnings'
self.filters = FilterOptionsKiBot self.filters = FilterOptionsKiBot

View File

@ -151,6 +151,7 @@ class GS(object):
global_field_tolerance = None global_field_tolerance = None
global_field_voltage = None global_field_voltage = None
global_hide_excluded = None global_hide_excluded = None
global_git_diff_strategy = None
global_impedance_controlled = None global_impedance_controlled = None
global_invalidate_pcb_text_cache = None global_invalidate_pcb_text_cache = None
global_kiauto_time_out_scale = None global_kiauto_time_out_scale = None

View File

@ -154,9 +154,9 @@ class DiffOptions(BaseOptions):
self.name_used_for_cache = name self.name_used_for_cache = name
run_command(cmd) run_command(cmd)
def cache_pcb(self, name): def cache_pcb(self, name, force_exist):
if name: if name:
if not os.path.isfile(name): if not os.path.isfile(name) and not force_exist:
raise KiPlotConfigurationError('Missing file to compare: `{}`'.format(name)) raise KiPlotConfigurationError('Missing file to compare: `{}`'.format(name))
else: else:
GS.check_pcb() GS.check_pcb()
@ -172,17 +172,17 @@ class DiffOptions(BaseOptions):
self.add_to_cache(name, hash) self.add_to_cache(name, hash)
return hash return hash
def cache_sch(self, name): def cache_sch(self, name, force_exist):
if name: if name:
if not os.path.isfile(name): if not os.path.isfile(name) and not force_exist:
raise KiPlotConfigurationError('Missing file to compare: `{}`'.format(name)) raise KiPlotConfigurationError('Missing file to compare: `{}`'.format(name))
else: else:
GS.check_sch() GS.check_sch()
name = GS.sch_file name = GS.sch_file
ext = os.path.splitext(name)[1]
if not os.path.isfile(name): if not os.path.isfile(name):
if self.always_fail_if_missing: if self.always_fail_if_missing:
raise KiPlotConfigurationError('Missing file to compare: `{}`'.format(name)) raise KiPlotConfigurationError('Missing file to compare: `{}`'.format(name))
ext = os.path.splitext(name)[1]
with NamedTemporaryFile(mode='w', suffix=ext, delete=False) as f: with NamedTemporaryFile(mode='w', suffix=ext, delete=False) as f:
logger.debug('Creating empty schematic: '+f.name) logger.debug('Creating empty schematic: '+f.name)
if ext == '.kicad_sch': if ext == '.kicad_sch':
@ -210,9 +210,9 @@ class DiffOptions(BaseOptions):
self.add_to_cache(name, hash) self.add_to_cache(name, hash)
return hash return hash
def cache_file(self, name=None): def cache_file(self, name=None, force_exist=False):
self.git_hash = 'Current' if not name else 'FILE' self.git_hash = 'Current' if not name else 'FILE'
return self.cache_pcb(name) if self.pcb else self.cache_sch(name) return self.cache_pcb(name, force_exist) if self.pcb else self.cache_sch(name, force_exist)
def run_git(self, cmd, cwd=None, just_raise=False): def run_git(self, cmd, cwd=None, just_raise=False):
if cwd is None: if cwd is None:
@ -240,7 +240,7 @@ class DiffOptions(BaseOptions):
logger.debug('Git submodules '+str(subs)) logger.debug('Git submodules '+str(subs))
return subs return subs
def undo_git(self): def undo_git_use_stash(self):
if self.checkedout: if self.checkedout:
logger.debug('Restoring point '+self.branch) logger.debug('Restoring point '+self.branch)
self.run_git(['checkout', '--force', '--recurse-submodules', self.branch]) self.run_git(['checkout', '--force', '--recurse-submodules', self.branch])
@ -303,23 +303,23 @@ class DiffOptions(BaseOptions):
return self.solve_kibot_magic(name, 'KIBOT_TAG') return self.solve_kibot_magic(name, 'KIBOT_TAG')
return name return name
def get_git_point_desc(self, user_name): def get_git_point_desc(self, user_name, cwd=None):
# Are we at a tagged point? # Are we at a tagged point?
name = None name = None
try: try:
name = self.run_git(['describe', '--exact-match', '--tags', 'HEAD'], just_raise=True) name = self.run_git(['describe', '--exact-match', '--tags', 'HEAD'], cwd=cwd, just_raise=True)
if user_name == 'Dirty': if user_name == 'Dirty':
name += '-dirty' name += '-dirty'
except CalledProcessError: except CalledProcessError:
logger.debug("Not at a tag point") logger.debug("Not at a tag point")
if name is None: if name is None:
# Are we at the HEAD of a branch? # Are we at the HEAD of a branch?
branch = self.run_git(['rev-parse', '--abbrev-ref', 'HEAD']) branch = self.run_git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd=cwd)
if branch == 'HEAD': if branch == 'HEAD':
# Detached state # Detached state
# Try to find the name relative to a tag # Try to find the name relative to a tag
try: try:
name = self.run_git(['describe', '--tags', '--dirty'], just_raise=True) name = self.run_git(['describe', '--tags', '--dirty'], cwd=cwd, just_raise=True)
except CalledProcessError: except CalledProcessError:
logger.debug("Can't find a tag name") logger.debug("Can't find a tag name")
if not name: if not name:
@ -331,19 +331,19 @@ class DiffOptions(BaseOptions):
# Do we have a tag in this branch # Do we have a tag in this branch
extra = None extra = None
try: try:
extra = self.run_git(['describe', '--tags', '--abbrev=0', '--dirty'], just_raise=True) extra = self.run_git(['describe', '--tags', '--abbrev=0', '--dirty'], cwd=cwd, just_raise=True)
except CalledProcessError: except CalledProcessError:
logger.debug("Can't find a tag name") logger.debug("Can't find a tag name")
if extra: if extra:
name += '['+extra+']' name += '['+extra+']'
elif user_name == 'Dirty': elif user_name == 'Dirty':
name += '-dirty' name += '-dirty'
return '{}({})'.format(self.run_git(['rev-parse', '--short', 'HEAD']), name) return '{}({})'.format(self.run_git(['rev-parse', '--short', 'HEAD'], cwd=cwd), name)
def git_dirty(self): def git_dirty(self):
return self.run_git(['status', '--porcelain', '-uno']) return self.run_git(['status', '--porcelain', '-uno'])
def cache_git(self, name): def cache_git_use_stash(self, name):
self.stashed = False self.stashed = False
self.checkedout = False self.checkedout = False
# Which file # Which file
@ -385,9 +385,49 @@ class DiffOptions(BaseOptions):
# A short version of the current hash # A short version of the current hash
self.git_hash = self.get_git_point_desc(name_ori) self.git_hash = self.get_git_point_desc(name_ori)
finally: finally:
self.undo_git() self.undo_git_use_stash()
return hash return hash
def cache_git(self, name):
self.stashed = False
self.checkedout = False
# Which file
if self.pcb:
GS.check_pcb()
self.file = GS.pcb_file
else:
GS.check_sch()
self.file = GS.sch_file
# Place where we know we have a repo
self.repo_dir = os.path.dirname(os.path.abspath(self.file))
if name:
# Checkout the target
name_ori = name
name = self.solve_git_name(name)
git_tmp_wd = mkdtemp()
logger.debug('Checking out '+name+' to '+git_tmp_wd)
self.run_git(['worktree', 'add', git_tmp_wd, name])
self._worktrees_to_remove.append(git_tmp_wd)
self.run_git(['submodule', 'init'], cwd=git_tmp_wd)
self.run_git(['submodule', 'update'], cwd=git_tmp_wd)
name_copy = self.run_git(['ls-files', '--full-name', self.file])
name_copy = os.path.join(git_tmp_wd, name_copy)
logger.debug('- Using temporal copy: '+name_copy)
hash = self.cache_file(name_copy, force_exist=True)
cwd = git_tmp_wd
else:
name_ori = 'Dirty' if self.git_dirty() else 'HEAD'
# Populate the cache
hash = self.cache_file()
cwd = self.repo_dir
# A short version of the current hash
self.git_hash = self.get_git_point_desc(name_ori, cwd)
return hash
def remove_git_worktree(self, name):
logger.debug('Removing temporal checkout at '+name)
self.run_git(['worktree', 'remove', '--force', name])
@staticmethod @staticmethod
def check_output_type(out, must_be): def check_output_type(out, must_be):
if out.type != must_be: if out.type != must_be:
@ -422,16 +462,16 @@ class DiffOptions(BaseOptions):
fname, dir_name = VariantOptions.save_tmp_dir_board('diff') fname, dir_name = VariantOptions.save_tmp_dir_board('diff')
else: else:
dir_name = mkdtemp() dir_name = mkdtemp()
self.dirs_to_remove.append(dir_name)
fname = GS.sch.save_variant(dir_name) fname = GS.sch.save_variant(dir_name)
GS.copy_project_sch(dir_name) GS.copy_project_sch(dir_name)
self.dirs_to_remove.append(dir_name)
res = self.cache_file(os.path.join(dir_name, fname)) res = self.cache_file(os.path.join(dir_name, fname))
self.git_hash = 'Current' self.git_hash = 'Current'
return res return res
def cache_obj(self, name, type): def cache_obj(self, name, type):
if type == 'git': if type == 'git':
return self.cache_git(name) return self.cache_git(name) if GS.global_git_diff_strategy == 'worktree' else self.cache_git_use_stash(name)
if type == 'file': if type == 'file':
return self.cache_file(name) return self.cache_file(name)
if type == 'current': if type == 'current':
@ -498,6 +538,7 @@ class DiffOptions(BaseOptions):
def run(self, name): def run(self, name):
self.command = self.ensure_tool('KiDiff') self.command = self.ensure_tool('KiDiff')
self._to_remove = [] self._to_remove = []
self._worktrees_to_remove = []
if self.old_type == 'git' or self.new_type == 'git': if self.old_type == 'git' or self.new_type == 'git':
self.git_command = self.ensure_tool('Git') self.git_command = self.ensure_tool('Git')
if not self.pcb: if not self.pcb:
@ -543,6 +584,9 @@ class DiffOptions(BaseOptions):
os.remove(self.incl_file) os.remove(self.incl_file)
for f in self._to_remove: for f in self._to_remove:
os.remove(f) os.remove(f)
# Remove any git worktree that we created
for w in self._worktrees_to_remove:
self.remove_git_worktree(w)
@output_class @output_class

View File

@ -1400,7 +1400,8 @@ def test_diff_git_2(test_dir):
msg = f.read() msg = f.read()
assert msg == 'Bye!\n' assert msg == 'Bye!\n'
# Check the link # Check the link
assert glob(os.path.join(ctx.output_dir, prj+'-diff_pcb_*(v1)-*(master[[]v1[]]).pdf')) assert (glob(os.path.join(ctx.output_dir, prj+'-diff_pcb_*(v1)-*(master[[]v1[]]).pdf')) +
glob(os.path.join(ctx.output_dir, prj+'-diff_pcb_*(v1)-*(v1-2-*).pdf'))), "Can't find link"
ctx.clean_up(keep_project=True) ctx.clean_up(keep_project=True)