Source

hgsubversion hacking / tag-refactor.diff

Full commit
  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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# HG changeset patch
# User Dan Villiom Podlaski Christiansen <danchr@gmail.com>
# Date 1243000581 -7200
# Node ID 8c08c8b2ad97241744a527a6f2ae039773fb19ed
# Parent  ffd50e9d982b6409046a77cf5f509c9a69bf48b1
This is it! The Tag Refactor(tm) - convert tags to native Mercurial '.hgtags'.

There are some caveats at this point:
- I haven't converted stupid fetching.
- Some tests fail. Some of them should fail, in my opinion. Some of
  them just need to have their hashes updated.

Some miscellany too difficult to split out into a separate commit:
- Rename __determine_parent_branch() to lessen the risk of dangerous
  inflation of underscores.

It's getting very late, and I'm quite tired now. I'm sure there's
something I forgot to mention...

diff --git a/hgsubversion/hg_delta_editor.py b/hgsubversion/hg_delta_editor.py
--- a/hgsubversion/hg_delta_editor.py
+++ b/hgsubversion/hg_delta_editor.py
@@ -110,10 +110,6 @@
             self.branches = pickle.load(f)
             f.close()
         self.tags = {}
-        if os.path.exists(self.tag_info_file):
-            f = open(self.tag_info_file)
-            self.tags = pickle.load(f)
-            f.close()
         if os.path.exists(self.tag_locations_file):
             f = open(self.tag_locations_file)
             self.tag_locations = pickle.load(f)
@@ -195,13 +191,14 @@
         self.base_revision = None
         self.branches_to_delete = set()
         self.externals = {}
+        self.added_tags = {}
+        self.tags_to_delete = set()
 
     def _save_metadata(self):
         '''Save the Subversion metadata. This should really be called after
         every revision is created.
         '''
         pickle_atomic(self.branches, self.branch_info_file, self.meta_data_dir)
-        pickle_atomic(self.tags, self.tag_info_file, self.meta_data_dir)
 
     def branches_in_paths(self, paths, revnum, checkpath, listdir):
         '''Given a list of paths, return mapping of all branches touched
@@ -479,6 +476,8 @@
         return 'branches/%s' % branch
 
     def _determine_parent_branch(self, p, src_path, src_rev, revnum):
+        tag = self._is_path_tag(p)
+
         if src_path is not None:
             src_file, src_branch = self._path_and_branch_for_path(src_path)
             src_tag = self._is_path_tag(src_path)
@@ -489,37 +488,77 @@
             if src_file == '':
                 # case 2
                 return {self._localname(p): (src_branch, src_rev, revnum )}
+        elif tag and tag in self.tags:
+            # Check if this is a tag, if so transform it into a branch
+            src_branch, src_rev = self.tags[tag]
+            self.tags_to_delete.add(tag)
+            return { self._split_tag_path(p)[1]: (src_branch, src_rev, revnum) }
+        else:
+            src_file, src_branch = self._path_and_branch_for_path(p)
+            if src_file == '':
+                # case 2
+                return {
+                    self._split_tag_path(p)[1]: (src_branch, src_rev, revnum)
+                    }
+            elif src_file:
+                self.ui.warn('WARNING: Peculiar tag %s on branch %s (%s)\n'
+                             % (tag, src_file, src_branch))
+
         return {}
 
     def update_branch_tag_map_for_rev(self, revision):
         paths = revision.paths
         added_branches = {}
-        added_tags = {}
+        self.added_tags = {}
         self.branches_to_delete = set()
-        tags_to_delete = set()
+        self.tags_to_delete = set()
+        # each entry is a tuple of (topic, message)
+        warning_msgs = set()
         for p in sorted(paths):
-            t_name = self._is_path_tag(p)
-            if t_name != False:
+            t_name, relpath = self._split_tag_path(p)[1:]
+            if t_name:
+                shouldtag = False
+                shouldignore = False
                 src_p, src_rev = paths[p].copyfrom_path, paths[p].copyfrom_rev
-                # if you commit to a tag, I'm calling you stupid and ignoring
-                # you.
                 if src_p is not None and src_rev is not None:
                     file, branch = self._path_and_branch_for_path(src_p)
                     if file is None:
                         # some crazy people make tags from other tags
                         file = ''
                         from_tag = self._is_path_tag(src_p)
+                        # Do something when the source isn't found; perhaps 
+                        # delete the path from the revision?
                         if not from_tag:
                             continue
-                        branch, src_rev = self.tags[from_tag]
-                    if t_name not in added_tags:
-                        added_tags[t_name] = branch, src_rev
-                    elif file and src_rev > added_tags[t_name][1]:
-                        added_tags[t_name] = branch, src_rev
-                elif (paths[p].action == 'D' and p.endswith(t_name)
-                      and t_name in self.tags):
-                        tags_to_delete.add(t_name)
-                continue
+                        else:
+                            shouldtag = True
+                            branch, src_rev = self.tags[from_tag]
+                    elif t_name not in self.added_tags:
+                        shouldtag = True
+                    elif file and src_rev > self.added_tags[t_name][1]:
+                        shouldtag = True
+                elif (t_name in self.tags and 
+                      paths[p].action == 'D' and relpath[3:] == p):
+                    self.tags_to_delete.add(t_name)
+                    continue
+                else:
+                    # This tag will be transformed into a branch
+                    shouldignore = True
+
+                if shouldignore:
+                    if shouldignore is not True:
+                        warning_msgs.add((t_name, shouldignore))
+                    if t_name in self.added_tags:
+                        del self.added_tags[t_name]
+                    br = self._determine_parent_branch \
+                        (p, paths[p].copyfrom_path, paths[p].copyfrom_rev,
+                         revision.revnum)
+                    added_branches.update(br)
+
+                elif shouldtag:
+                    self.tags[t_name] = self.added_tags[t_name] = \
+                        branch, src_rev
+                    continue
             # At this point we know the path is not a tag. In that
             # case, we only care if it is the root of a new branch (in
             # this function). This is determined by the following
@@ -564,18 +603,108 @@
                 if bpath is not None and branch not in self.branches:
                     parent = {branch: (None, 0, revision.revnum)}
             added_branches.update(parent)
-        for t in tags_to_delete:
+
+        for tag, msg in warning_msgs:
+            self.ui.warn('WARNING: Unconverted operation on tag %s: %s.\n'
+                         % (tag, msg))
+
+        for t in self.tags_to_delete:
+            self.ui.status('Untagging %s\n' % t)
             del self.tags[t]
+
+        for t, (b, r) in self.added_tags.iteritems():
+            if b:
+                b = 'branch %s' % b
+            else:
+                b = 'trunk'
+            self.ui.status('Tagging %s from %s, revision %d\n' % (t, b, r))
+
         for br in self.branches_to_delete:
+            if not br or br == 'default':
+                self.ui.warn('WARNING: Attempting to close the main branch; '
+                             'this may be a bug in hgsubversion rather than an '
+                             'intended operation!')
+
+            self.ui.status('Closing branch %s\n' % br)
             del self.branches[br]
-        for t, info in added_tags.items():
-            self.ui.status('Tagged %s@%s as %s\n' %
-                           (info[0] or 'trunk', info[1], t))
-        self.tags.update(added_tags)
+
+
+        for b, (sb, sr, r) in added_branches.iteritems():
+            if b or sb:
+                if sb:
+                    sb = 'branch %s' % sb
+                else:
+                    sb = 'trunk'
+
+                self.ui.status('Branching %s from %s, revision %d\n'
+                               % (b or 'default', sb, r))
         self.branches.update(added_branches)
         self._save_metadata()
 
-    def _updateexternals(self):
+    def _update_generated_files(self):
+        self._update_externals()
+        self._update_tags()
+
+    def _update_tags(self):
+        if not self.added_tags and not self.tags_to_delete:
+            return
+
+        # FIXME: for now, we add tags to all branches
+        # later on, we want to restrict additions to the relevant branches
+        branches = set(self.branches.iterkeys())
+
+        # Synthesise the new .hgtags file, retaining the tagging commit.
+        for bname in branches:
+            touched = False
+            parent = self.get_parent_revision(self.current_rev.revnum,
+                                              bname)
+            try:
+                branch = self.repo[self.repo.branchheads(bname)[0]]
+                pctx = self.repo[parent]
+
+                # Deserialise tag data, if necessary.
+                if '.hgtags' in pctx:
+                    hgtags = dict([reversed(l.split(None, 1))
+                                   for l in pctx['.hgtags'].data().splitlines()
+                                   if l.strip()])
+                else:
+                    hgtags = {}
+            except IndexError, e:
+                hgtags = {}
+                branch = {}
+
+            if bname is None:
+                path = 'trunk/.hgtags'
+            else:
+                path = 'branches/%s/.hgtags' % bname
+
+            for t in self.tags_to_delete:
+                if t in hgtags:
+                    touched = True
+                    del hgtags[t]
+            for t, (b, r) in self.added_tags.iteritems():
+                touched = True
+                hgtags[t] = node.hex(self.get_parent_revision(r + 1, b))
+
+            # If no tags are left, delete!
+            if not hgtags:
+                if '.hgtags' in branch:
+                    self.delete_file(path)
+                return
+            # If no changes were necessary, don't do anything.
+            elif not touched:
+                return
+
+            if '.hgtags' not in branch:
+                self.add_file(path)
+
+            # Serialise and queue tag file
+            hgtags = ['%s %s' % (n, t) for t, n in hgtags.iteritems()]
+            hgtags = '\n'.join(hgtags)
+            self.set_file(path, hgtags)
+            print hgtags
+
+    def _update_externals(self):
         if not self.externals:
             return
         # Accumulate externals records for all branches
@@ -611,7 +740,7 @@
             raise ReplayException()
         if self.missing_plaintexts:
             raise MissingPlainTextError()
-        self._updateexternals()
+        self._update_generated_files()
         # paranoidly generate the list of files to commit
         files_to_commit = set(self.current_files.keys())
         files_to_commit.update(self.current_files_symlink.keys())
@@ -827,10 +956,6 @@
         return self.meta_file_named('branch_info')
     branch_info_file = property(branch_info_file)
 
-    def tag_info_file(self):
-        return self.meta_file_named('tag_info')
-    tag_info_file = property(tag_info_file)
-
     def tag_locations_file(self):
         return self.meta_file_named('tag_locations')
     tag_locations_file = property(tag_locations_file)
diff --git a/hgsubversion/svnrepo.py b/hgsubversion/svnrepo.py
--- a/hgsubversion/svnrepo.py
+++ b/hgsubversion/svnrepo.py
@@ -29,16 +29,6 @@
 
     superclass = repo.__class__
 
-    def localsvn(fn):
-        """
-        Filter for instance methods which only apply to local Subversion
-        repositories.
-        """
-        if util.is_svn_repo(repo):
-            return fn
-        else:
-            return getattr(repo, fn.__name__)
-
     def remotesvn(fn):
         """
         Filter for instance methods which require the first argument
@@ -74,15 +64,6 @@
             raise hgutil.Abort('cannot display incoming changes from '
                                'Subversion repositories, yet')
 
-        @localsvn
-        def tags(self):
-            tags = superclass.tags(self)
-            hg_editor = hg_delta_editor.HgChangeReceiver(repo=self)
-            for tag, source in hg_editor.tags.iteritems():
-                target = hg_editor.get_parent_revision(source[1]+1, source[0])
-                tags['tag/%s' % tag] = target
-            return tags
-
     repo.__class__ = svnlocalrepo
 
 class svnremoterepo(mercurial.repo.repository):
diff --git a/tests/test_fetch_branches.py b/tests/test_fetch_branches.py
--- a/tests/test_fetch_branches.py
+++ b/tests/test_fetch_branches.py
@@ -21,11 +21,24 @@
         repo = hg.clone(ui.ui(), source=source, dest=self.wc_path)
         return hg.repository(ui.ui(), self.wc_path)
 
+    def _getheads(self, fixture, stupid):
+        repo = self._load_fixture_and_fetch(fixture, stupid)
+        heads = [repo[n] for n in repo.heads()]
+        return dict([(ctx.branch(), ctx) for ctx in heads])
+
+
+    def test_relatedbranch(self, stupid=False):
+        heads = self._getheads('unrelatedbranch.svndump', stupid)
+        self.assertEqual(heads['branch2'].manifest().keys(), ['a', 'b'])
+
+    def test_relatedbranch_stupid(self):
+        self.test_relatedbranch(True)
+
     def test_unrelatedbranch(self, stupid=False):
-        repo = self._load_fixture_and_fetch('unrelatedbranch.svndump', stupid)
-        heads = [repo[n] for n in repo.heads()]
-        heads = dict([(ctx.branch(), ctx) for ctx in heads])
+        heads = self._getheads('unrelatedbranch.svndump', stupid)
         # Let these tests disabled yet as the fix is not obvious
+        # Do we even want to convert distinct, non-derivative branches?
+        self.assertFalse('branch1' in heads, 'unexpected success')
         self.assertEqual(heads['branch1'].manifest().keys(), ['b'])
         self.assertEqual(heads['branch2'].manifest().keys(), ['a', 'b'])
 
diff --git a/tests/test_tags.py b/tests/test_tags.py
--- a/tests/test_tags.py
+++ b/tests/test_tags.py
@@ -26,8 +26,8 @@
                                             stupid=stupid)
         self._test_tag_revision_info(repo)
         repo = self.repo
-        self.assertEqual(repo['tip'].node(), repo['tag/tag_r3'].node())
-        self.assertEqual(repo['tip'].node(), repo['tag/copied_tag'].node())
+        self.assertEqual(repo['tip'].node(), repo['tag_r3'].node())
+        self.assertEqual(repo['tip'].node(), repo['copied_tag'].node())
 
     def test_tags_stupid(self):
         self.test_tags(stupid=True)
@@ -37,8 +37,8 @@
                                             stupid=stupid)
         self._test_tag_revision_info(repo)
         repo = self.repo
-        self.assertEqual(repo['tip'].node(), repo['tag/tag_r3'].node())
-        self.assert_('tag/copied_tag' not in repo.tags())
+        self.assertEqual(repo['tip'].node(), repo['tag_r3'].node())
+        self.assert_('copied_tag' not in repo.tags())
 
     def test_remove_tag_stupid(self):
         self.test_remove_tag(stupid=True)
@@ -48,9 +48,9 @@
                                             stupid=stupid)
         self._test_tag_revision_info(repo)
         repo = self.repo
-        self.assertEqual(repo['tip'].node(), repo['tag/tag_r3'].node())
-        self.assertEqual(repo['tip'].node(), repo['tag/other_tag_r3'].node())
-        self.assert_('tag/copied_tag' not in repo.tags())
+        self.assertEqual(repo['tip'].node(), repo['tag_r3'].node())
+        self.assertEqual(repo['tip'].node(), repo['other_tag_r3'].node())
+        self.assert_('copied_tag' not in repo.tags())
 
     def test_rename_tag_stupid(self):
         self.test_rename_tag(stupid=True)
@@ -60,9 +60,9 @@
                                             stupid=stupid)
         repo = self.repo
         self.assertEqual(repo['tip'].node(), repo['branch_from_tag'].node())
-        self.assertEqual(repo[1].node(), repo['tag/tag_r3'].node())
+        self.assertEqual(repo[1].node(), repo['tag_r3'].node())
         self.assertEqual(repo['branch_from_tag'].parents()[0].node(),
-                         repo['tag/copied_tag'].node())
+                         repo['copied_tag'].node())
 
     def test_branch_from_tag_stupid(self):
         self.test_branch_from_tag(stupid=True)
@@ -77,7 +77,7 @@
         taggedrev = repo['tip'].parents()[0]
         self.assertEqual(node.hex(taggedrev.node()),
                          '50c67c73267987de705ee335183c5486641e56e9')
-        self.assertEqual(node.hex(repo['tag/dummy'].node()),
+        self.assertEqual(node.hex(repo['dummy'].node()),
                          '50c67c73267987de705ee335183c5486641e56e9')
 
     def test_tag_by_renaming_branch_stupid(self):