1313from bugwarrior .notifications import send_notification
1414
1515if TYPE_CHECKING :
16+ from bugwarrior .config .schema import MainSectionConfig
1617 from bugwarrior .config .validation import Config
1718
1819log = logging .getLogger (__name__ )
@@ -26,29 +27,10 @@ class MultipleMatches(Exception):
2627 pass
2728
2829
29- def get_normalized_annotation (annotation : str ) -> str :
30+ def normalize_annotation (annotation : str ) -> str :
3031 return re .sub (r'[\W_]' , '' , str (annotation ))
3132
3233
33- def get_annotation_hamming_distance (left : str , right : str ) -> int :
34- left = get_normalized_annotation (left )
35- right = get_normalized_annotation (right )
36- if len (left ) > len (right ):
37- left = left [0 : len (right )]
38- elif len (right ) > len (left ):
39- right = right [0 : len (left )]
40- return hamdist (left , right )
41-
42-
43- def hamdist (str1 : str , str2 : str ) -> int :
44- """Count the # of differences between equal length strings str1 and str2"""
45- diffs = 0
46- for ch1 , ch2 in zip (str1 , str2 ):
47- if ch1 != ch2 :
48- diffs += 1
49- return diffs
50-
51-
5234def get_managed_task_uuids (
5335 tw : TaskWarriorShellout , key_list : dict [str , list [str ]]
5436) -> set [str ]:
@@ -155,99 +137,36 @@ def find_taskwarrior_uuid(
155137 raise NotFound ("No issue was found matching %s" % issue )
156138
157139
158- def replace_left (
159- field : str ,
160- local_task : dict [str , Any ],
161- remote_issue : dict [str , Any ],
162- keep_items : list [str ] = [],
163- ) -> None :
164- """Replace array field from the remote_issue to the local_task
165-
166- * Local 'left' entries are suppressed, unless those listed in keep_items.
167- * Remote 'left' are appended to task, if not present in local.
168-
169- :param `field`: Task field to merge.
170- :param `local_task`: `taskw.task.Task` object into which to replace
171- remote changes.
172- :param `remote_issue`: `dict` instance from which to add into
173- local task.
174- :param `keep_items`: list of items to keep into local_task even if not
175- present in remote_issue
176- """
140+ def are_normalized_annotations_equal (left : str , right : str ) -> bool :
141+ _left , _right = map (normalize_annotation , (left , right ))
142+ min_length = min (len (_left ), len (_right ))
143+ return _left [:min_length ] == _right [:min_length ]
177144
178- # Ensure that empty default are present
179- local_field = local_task .get (field , []).copy ()
180- remote_field = remote_issue .get (field , [])
181-
182- # We need to make sure an array exists for this field because
183- # we will be appending to it in a moment.
184- if field not in local_task :
185- local_task [field ] = []
186-
187- # Delete all items in local_task, unless they are in keep_items or in remote_issue
188- # This ensure that the task is not being updated if there is no changes
189- for item in local_field :
190- if keep_items .count (item ) == 0 and remote_field .count (item ) == 0 :
191- log .debug ('found %s to remove' % (item ))
192- local_task [field ].remove (item )
193- elif remote_field .count (item ) > 0 :
194- remote_field .remove (item )
195-
196- if len (remote_field ) > 0 :
197- local_task [field ] += remote_field
198-
199-
200- def merge_left (
201- field : str ,
202- local_task : dict [str , Any ],
203- remote_issue : dict [str , Any ],
204- hamming : bool = False ,
205- ) -> None :
206- """Merge array field from the remote_issue into local_task
207145
208- * Local 'left' entries are preserved without modification
209- * Remote 'left' are appended to task if not present in local.
146+ def merge_annotations (local : dict [str , Any ], remote : dict [str , Any ]) -> list [str ]:
147+ """
148+ Merge annotations. Order and duplication are preserved.
149+ """
150+ local_annotations = local .get ("annotations" , [])
151+ new_annotations = [
152+ annotation
153+ for annotation in remote .get ("annotations" , [])
154+ if not any (
155+ are_normalized_annotations_equal (annotation , local_annotation )
156+ for local_annotation in local_annotations
157+ )
158+ ]
159+ return [* local_annotations , * new_annotations ]
210160
211- :param `field`: Task field to merge.
212- :param `local_task`: `taskw.task.Task` object into which to merge
213- remote changes.
214- :param `remote_issue`: `dict` instance from which to merge into
215- local task.
216- :param `hamming`: (default `False`) If `True`, compare entries by
217- truncating to maximum length, and comparing hamming distances.
218- Useful generally only for annotations.
219161
220- """
162+ def merge_tags (
163+ main_conf : "MainSectionConfig" , local : dict [str , Any ], remote : dict [str , Any ]
164+ ) -> list [str ]:
165+ task_tags : set [str ] = set (local .get ("tags" , []))
166+ if main_conf .replace_tags :
167+ task_tags &= set (main_conf .static_tags )
221168
222- # Ensure that empty defaults are present
223- local_field = local_task .get (field , [])
224- remote_field = remote_issue .get (field , [])
225-
226- # We need to make sure an array exists for this field because
227- # we will be appending to it in a moment.
228- if field not in local_task :
229- local_task [field ] = []
230-
231- # If a remote does not appear in local, add it to the local task
232- new_count = 0
233- for remote in remote_field :
234- for local in local_field :
235- if (
236- # For annotations, they don't have to match *exactly*.
237- (hamming and get_annotation_hamming_distance (remote , local ) == 0 )
238- # But for everything else, they should.
239- or (remote == local )
240- ):
241- break
242- else :
243- log .debug ("%s not found in %r" % (remote , local_field ))
244- local_task [field ].append (remote )
245- new_count += 1
246- if new_count > 0 :
247- log .debug (
248- 'Added %s new values to %s (total: %s)'
249- % (new_count , field , len (local_task [field ]))
250- )
169+ return sorted (task_tags | set (remote .get ("tags" , [])))
251170
252171
253172def run_hooks (pre_import : list [str ]) -> None :
@@ -357,13 +276,10 @@ def synchronize(
357276
358277 # Merge annotations & tags from online into our task object
359278 if conf .main .merge_annotations :
360- merge_left ( ' annotations' , task , issue , hamming = True )
279+ task [ " annotations" ] = merge_annotations ( task , issue )
361280
362281 if conf .main .merge_tags :
363- if conf .main .replace_tags :
364- replace_left ('tags' , task , issue , list (conf .main .static_tags ))
365- else :
366- merge_left ('tags' , task , issue )
282+ task ["tags" ] = merge_tags (conf .main , task , issue )
367283
368284 issue .pop ('annotations' , None )
369285 issue .pop ('tags' , None )
0 commit comments