comparison hgext/mq.py @ 2810:2e4ace008c94

mq: new commands qselect, qguard implement quilt-style guards for mq. guards allow to control whether patch can be pushed. if guard X is active and patch is guarded by +X (called "posative guard"), patch can be pushed. if patch is guarded by -X (called "nagative guard"), patch cannot be pushed and is skipped. use qguard to set/list guards on patches. use qselect to set/list active guards. also "qseries -v" prints guarded patches with "G" now.
author Vadim Gelfer <vadim.gelfer@gmail.com>
date Tue, 08 Aug 2006 21:42:50 -0700
parents 766ecdc83e43
children 12139eedd6a0
comparison
equal deleted inserted replaced
2809:1bb8dd08c594 2810:2e4ace008c94
63 self.full_series = [] 63 self.full_series = []
64 self.applied_dirty = 0 64 self.applied_dirty = 0
65 self.series_dirty = 0 65 self.series_dirty = 0
66 self.series_path = "series" 66 self.series_path = "series"
67 self.status_path = "status" 67 self.status_path = "status"
68 self.guards_path = "guards"
69 self.active_guards = None
70 self.guards_dirty = False
68 71
69 if os.path.exists(self.join(self.series_path)): 72 if os.path.exists(self.join(self.series_path)):
70 self.full_series = self.opener(self.series_path).read().splitlines() 73 self.full_series = self.opener(self.series_path).read().splitlines()
71 self.parse_series() 74 self.parse_series()
72 75
73 if os.path.exists(self.join(self.status_path)): 76 if os.path.exists(self.join(self.status_path)):
74 self.applied = [statusentry(l) 77 lines = self.opener(self.status_path).read().splitlines()
75 for l in self.opener(self.status_path).read().splitlines()] 78 self.applied = [statusentry(l) for l in lines]
76 79
77 def join(self, *p): 80 def join(self, *p):
78 return os.path.join(self.path, *p) 81 return os.path.join(self.path, *p)
79 82
80 def find_series(self, patch): 83 def find_series(self, patch):
88 if s == patch: 91 if s == patch:
89 return index 92 return index
90 index += 1 93 index += 1
91 return None 94 return None
92 95
96 guard_re = re.compile(r'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)')
97
93 def parse_series(self): 98 def parse_series(self):
94 self.series = [] 99 self.series = []
100 self.series_guards = []
95 for l in self.full_series: 101 for l in self.full_series:
96 s = l.split('#', 1)[0].strip() 102 h = l.find('#')
97 if s: 103 if h == -1:
98 self.series.append(s) 104 patch = l
105 comment = ''
106 elif h == 0:
107 continue
108 else:
109 patch = l[:h]
110 comment = l[h:]
111 patch = patch.strip()
112 if patch:
113 self.series.append(patch)
114 self.series_guards.append(self.guard_re.findall(comment))
115
116 def check_guard(self, guard):
117 bad_chars = '# \t\r\n\f'
118 first = guard[0]
119 for c in '-+':
120 if first == c:
121 return (_('guard %r starts with invalid character: %r') %
122 (guard, c))
123 for c in bad_chars:
124 if c in guard:
125 return _('invalid character in guard %r: %r') % (guard, c)
126
127 def set_active(self, guards):
128 for guard in guards:
129 bad = self.check_guard(guard)
130 if bad:
131 raise util.Abort(bad)
132 guards = dict.fromkeys(guards).keys()
133 guards.sort()
134 self.ui.debug('active guards: %s\n' % ' '.join(guards))
135 self.active_guards = guards
136 self.guards_dirty = True
137
138 def active(self):
139 if self.active_guards is None:
140 self.active_guards = []
141 try:
142 guards = self.opener(self.guards_path).read().split()
143 except IOError, err:
144 if err.errno != errno.ENOENT: raise
145 guards = []
146 for i, guard in enumerate(guards):
147 bad = self.check_guard(guard)
148 if bad:
149 self.ui.warn('%s:%d: %s\n' %
150 (self.join(self.guards_path), i + 1, bad))
151 else:
152 self.active_guards.append(guard)
153 return self.active_guards
154
155 def set_guards(self, idx, guards):
156 for g in guards:
157 if len(g) < 2:
158 raise util.Abort(_('guard %r too short') % g)
159 if g[0] not in '-+':
160 raise util.Abort(_('guard %r starts with invalid char') % g)
161 bad = self.check_guard(g[1:])
162 if bad:
163 raise util.Abort(bad)
164 drop = self.guard_re.sub('', self.full_series[idx])
165 self.full_series[idx] = drop + ''.join([' #' + g for g in guards])
166 self.parse_series()
167 self.series_dirty = True
168
169 def pushable(self, idx):
170 if isinstance(idx, str):
171 idx = self.series.index(idx)
172 patchguards = self.series_guards[idx]
173 if not patchguards:
174 return True, None
175 default = False
176 guards = self.active()
177 exactneg = [g for g in patchguards if g[0] == '-' and g[1:] in guards]
178 if exactneg:
179 return False, exactneg[0]
180 pos = [g for g in patchguards if g[0] == '+']
181 exactpos = [g for g in pos if g[1:] in guards]
182 if pos:
183 if exactpos:
184 return True, exactpos[0]
185 return False, ''
186 return True, ''
187
188 def explain_pushable(self, idx, all_patches=False):
189 write = all_patches and self.ui.write or self.ui.warn
190 if all_patches or self.ui.verbose:
191 if isinstance(idx, str):
192 idx = self.series.index(idx)
193 pushable, why = self.pushable(idx)
194 if all_patches and pushable:
195 if why is None:
196 write(_('allowing %s - no guards in effect\n') %
197 self.series[idx])
198 else:
199 if not why:
200 write(_('allowing %s - no matching negative guards\n') %
201 self.series[idx])
202 else:
203 write(_('allowing %s - guarded by %r\n') %
204 (self.series[idx], why))
205 if not pushable:
206 if why and why[0] in '-+':
207 write(_('skipping %s - guarded by %r\n') %
208 (self.series[idx], why))
209 else:
210 write(_('skipping %s - no matching guards\n') %
211 self.series[idx])
99 212
100 def save_dirty(self): 213 def save_dirty(self):
101 def write_list(items, path): 214 def write_list(items, path):
102 fp = self.opener(path, 'w') 215 fp = self.opener(path, 'w')
103 for i in items: 216 for i in items:
104 print >> fp, i 217 print >> fp, i
105 fp.close() 218 fp.close()
106 if self.applied_dirty: write_list(map(str, self.applied), self.status_path) 219 if self.applied_dirty: write_list(map(str, self.applied), self.status_path)
107 if self.series_dirty: write_list(self.full_series, self.series_path) 220 if self.series_dirty: write_list(self.full_series, self.series_path)
221 if self.guards_dirty: write_list(self.active_guards, self.guards_path)
108 222
109 def readheaders(self, patch): 223 def readheaders(self, patch):
110 def eatdiff(lines): 224 def eatdiff(lines):
111 while lines: 225 while lines:
112 l = lines[-1] 226 l = lines[-1]
255 for patch in series: 369 for patch in series:
256 patch = mergeq.lookup(patch, strict=True) 370 patch = mergeq.lookup(patch, strict=True)
257 if not patch: 371 if not patch:
258 self.ui.warn("patch %s does not exist\n" % patch) 372 self.ui.warn("patch %s does not exist\n" % patch)
259 return (1, None) 373 return (1, None)
260 374 pushable, reason = self.pushable(patch)
375 if not pushable:
376 self.explain_pushable(patch, all_patches=True)
377 continue
261 info = mergeq.isapplied(patch) 378 info = mergeq.isapplied(patch)
262 if not info: 379 if not info:
263 self.ui.warn("patch %s is not applied\n" % patch) 380 self.ui.warn("patch %s is not applied\n" % patch)
264 return (1, None) 381 return (1, None)
265 rev = revlog.bin(info[1]) 382 rev = revlog.bin(info[1])
319 wlock = repo.wlock() 436 wlock = repo.wlock()
320 lock = repo.lock() 437 lock = repo.lock()
321 tr = repo.transaction() 438 tr = repo.transaction()
322 n = None 439 n = None
323 for patch in series: 440 for patch in series:
441 pushable, reason = self.pushable(patch)
442 if not pushable:
443 self.explain_pushable(patch, all_patches=True)
444 continue
324 self.ui.warn("applying %s\n" % patch) 445 self.ui.warn("applying %s\n" % patch)
325 pf = os.path.join(patchdir, patch) 446 pf = os.path.join(patchdir, patch)
326 447
327 try: 448 try:
328 message, comments, user, date, patchfound = self.readheaders(patch) 449 message, comments, user, date, patchfound = self.readheaders(patch)
637 sno = int(patch) 758 sno = int(patch)
638 except(ValueError, OverflowError): 759 except(ValueError, OverflowError):
639 pass 760 pass
640 else: 761 else:
641 if sno < len(self.series): 762 if sno < len(self.series):
642 patch = self.series[sno] 763 return self.series[sno]
643 return patch
644 if not strict: 764 if not strict:
645 # return any partial match made above 765 # return any partial match made above
646 if res: 766 if res:
647 return res 767 return res
648 minus = patch.rsplit('-', 1) 768 minus = patch.rsplit('-', 1)
924 raise util.Abort(_("patch %s is not in series file") % patch) 1044 raise util.Abort(_("patch %s is not in series file") % patch)
925 if not patch: 1045 if not patch:
926 start = self.series_end() 1046 start = self.series_end()
927 else: 1047 else:
928 start = self.series.index(patch) + 1 1048 start = self.series.index(patch) + 1
929 return [(i, self.series[i]) for i in xrange(start, len(self.series))] 1049 unapplied = []
1050 for i in xrange(start, len(self.series)):
1051 pushable, reason = self.pushable(i)
1052 if pushable:
1053 unapplied.append((i, self.series[i]))
1054 self.explain_pushable(i)
1055 return unapplied
930 1056
931 def qseries(self, repo, missing=None, summary=False): 1057 def qseries(self, repo, missing=None, summary=False):
932 start = self.series_end() 1058 start = self.series_end(all_patches=True)
933 if not missing: 1059 if not missing:
934 for i in range(len(self.series)): 1060 for i in range(len(self.series)):
935 patch = self.series[i] 1061 patch = self.series[i]
936 if self.ui.verbose: 1062 if self.ui.verbose:
937 if i < start: 1063 if i < start:
938 status = 'A' 1064 status = 'A'
1065 elif self.pushable(i)[0]:
1066 status = 'U'
939 else: 1067 else:
940 status = 'U' 1068 status = 'G'
941 self.ui.write('%d %s ' % (i, status)) 1069 self.ui.write('%d %s ' % (i, status))
942 if summary: 1070 if summary:
943 msg = self.readheaders(patch)[0] 1071 msg = self.readheaders(patch)[0]
944 msg = msg and ': ' + msg[0] or ': ' 1072 msg = msg and ': ' + msg[0] or ': '
945 else: 1073 else:
1058 if end == None: 1186 if end == None:
1059 return len(self.full_series) 1187 return len(self.full_series)
1060 return end + 1 1188 return end + 1
1061 return 0 1189 return 0
1062 1190
1063 def series_end(self): 1191 def series_end(self, all_patches=False):
1064 end = 0 1192 end = 0
1193 def next(start):
1194 if all_patches:
1195 return start
1196 i = start
1197 while i < len(self.series):
1198 p, reason = self.pushable(i)
1199 if p:
1200 break
1201 self.explain_pushable(i)
1202 i += 1
1203 return i
1065 if len(self.applied) > 0: 1204 if len(self.applied) > 0:
1066 p = self.applied[-1].name 1205 p = self.applied[-1].name
1067 try: 1206 try:
1068 end = self.series.index(p) 1207 end = self.series.index(p)
1069 except ValueError: 1208 except ValueError:
1070 return 0 1209 return 0
1071 return end + 1 1210 return next(end + 1)
1072 return end 1211 return next(end)
1073 1212
1074 def qapplied(self, repo, patch=None): 1213 def qapplied(self, repo, patch=None):
1075 if patch and patch not in self.series: 1214 if patch and patch not in self.series:
1076 raise util.Abort(_("patch %s is not in series file") % patch) 1215 raise util.Abort(_("patch %s is not in series file") % patch)
1077 if not patch: 1216 if not patch:
1370 for patch in patches: 1509 for patch in patches:
1371 q.delete(repo, patch, force=opts['force']) 1510 q.delete(repo, patch, force=opts['force'])
1372 1511
1373 q.save_dirty() 1512 q.save_dirty()
1374 1513
1514 def guard(ui, repo, *args, **opts):
1515 '''set or print guards for a patch
1516
1517 guards control whether a patch can be pushed. a patch with no
1518 guards is aways pushed. a patch with posative guard ("+foo") is
1519 pushed only if qselect command enables guard "foo". a patch with
1520 nagative guard ("-foo") is never pushed if qselect command enables
1521 guard "foo".
1522
1523 with no arguments, default is to print current active guards.
1524 with arguments, set active guards for patch.
1525
1526 to set nagative guard "-foo" on topmost patch ("--" is needed so
1527 hg will not interpret "-foo" as argument):
1528 hg qguard -- -foo
1529
1530 to set guards on other patch:
1531 hg qguard other.patch +2.6.17 -stable
1532 '''
1533 def status(idx):
1534 guards = q.series_guards[idx] or ['unguarded']
1535 ui.write('%s: %s\n' % (q.series[idx], ' '.join(guards)))
1536 q = repo.mq
1537 patch = None
1538 args = list(args)
1539 if opts['list']:
1540 if args or opts['none']:
1541 raise util.Abort(_('cannot mix -l/--list with options or arguments'))
1542 for i in xrange(len(q.series)):
1543 status(i)
1544 return
1545 if not args or args[0][0:1] in '-+':
1546 if not q.applied:
1547 raise util.Abort(_('no patches applied'))
1548 patch = q.applied[-1].name
1549 if patch is None and args[0][0:1] not in '-+':
1550 patch = args.pop(0)
1551 if patch is None:
1552 raise util.Abort(_('no patch to work with'))
1553 if args or opts['none']:
1554 q.set_guards(q.find_series(patch), args)
1555 q.save_dirty()
1556 else:
1557 status(q.series.index(q.lookup(patch)))
1558
1375 def header(ui, repo, patch=None): 1559 def header(ui, repo, patch=None):
1376 """Print the header of the topmost or specified patch""" 1560 """Print the header of the topmost or specified patch"""
1377 q = repo.mq 1561 q = repo.mq
1378 1562
1379 if patch: 1563 if patch:
1544 elif opts['nobackup']: 1728 elif opts['nobackup']:
1545 backup = 'none' 1729 backup = 'none'
1546 repo.mq.strip(repo, rev, backup=backup) 1730 repo.mq.strip(repo, rev, backup=backup)
1547 return 0 1731 return 0
1548 1732
1733 def select(ui, repo, *args, **opts):
1734 '''set or print guarded patches to push
1735
1736 use qguard command to set or print guards on patch. then use
1737 qselect to tell mq which guards to use. example:
1738
1739 qguard foo.patch -stable (nagative guard)
1740 qguard bar.patch +stable (posative guard)
1741 qselect stable
1742
1743 this sets "stable" guard. mq will skip foo.patch (because it has
1744 nagative match) but push bar.patch (because it has posative
1745 match).
1746
1747 with no arguments, default is to print current active guards.
1748 with arguments, set active guards as given.
1749
1750 use -n/--none to deactivate guards (no other arguments needed).
1751 when no guards active, patches with posative guards are skipped,
1752 patches with nagative guards are pushed.
1753
1754 use -s/--series to print list of all guards in series file (no
1755 other arguments needed). use -v for more information.'''
1756
1757 q = repo.mq
1758 guards = q.active()
1759 if args or opts['none']:
1760 q.set_active(args)
1761 q.save_dirty()
1762 if not args:
1763 ui.status(_('guards deactivated\n'))
1764 if q.series:
1765 pushable = [p for p in q.unapplied(repo) if q.pushable(p[0])[0]]
1766 ui.status(_('%d of %d unapplied patches active\n') %
1767 (len(pushable), len(q.series)))
1768 elif opts['series']:
1769 guards = {}
1770 noguards = 0
1771 for gs in q.series_guards:
1772 if not gs:
1773 noguards += 1
1774 for g in gs:
1775 guards.setdefault(g, 0)
1776 guards[g] += 1
1777 if ui.verbose:
1778 guards['NONE'] = noguards
1779 guards = guards.items()
1780 guards.sort(lambda a, b: cmp(a[0][1:], b[0][1:]))
1781 if guards:
1782 ui.note(_('guards in series file:\n'))
1783 for guard, count in guards:
1784 ui.note('%2d ' % count)
1785 ui.write(guard, '\n')
1786 else:
1787 ui.note(_('no guards in series file\n'))
1788 else:
1789 if guards:
1790 ui.note(_('active guards:\n'))
1791 for g in guards:
1792 ui.write(g, '\n')
1793 else:
1794 ui.write(_('no active guards\n'))
1795
1549 def version(ui, q=None): 1796 def version(ui, q=None):
1550 """print the version number of the mq extension""" 1797 """print the version number of the mq extension"""
1551 ui.write("mq version %s\n" % versionstr) 1798 ui.write("mq version %s\n" % versionstr)
1552 return 0 1799 return 0
1553 1800
1603 [('e', 'edit', None, _('edit patch header')), 1850 [('e', 'edit', None, _('edit patch header')),
1604 ('f', 'force', None, _('delete folded patch files')), 1851 ('f', 'force', None, _('delete folded patch files')),
1605 ('m', 'message', '', _('set patch header to <text>')), 1852 ('m', 'message', '', _('set patch header to <text>')),
1606 ('l', 'logfile', '', _('set patch header to contents of <file>'))], 1853 ('l', 'logfile', '', _('set patch header to contents of <file>'))],
1607 'hg qfold [-e] [-m <text>] [-l <file] PATCH...'), 1854 'hg qfold [-e] [-m <text>] [-l <file] PATCH...'),
1855 'qguard': (guard, [('l', 'list', None, _('list all patches and guards')),
1856 ('n', 'none', None, _('drop all guards'))],
1857 'hg qguard [PATCH] [+GUARD...] [-GUARD...]'),
1608 'qheader': (header, [], 1858 'qheader': (header, [],
1609 _('hg qheader [PATCH]')), 1859 _('hg qheader [PATCH]')),
1610 "^qimport": 1860 "^qimport":
1611 (qimport, 1861 (qimport,
1612 [('e', 'existing', None, 'import file in patch dir'), 1862 [('e', 'existing', None, 'import file in patch dir'),
1660 ('c', 'copy', None, 'copy patch directory'), 1910 ('c', 'copy', None, 'copy patch directory'),
1661 ('n', 'name', '', 'copy directory name'), 1911 ('n', 'name', '', 'copy directory name'),
1662 ('e', 'empty', None, 'clear queue status file'), 1912 ('e', 'empty', None, 'clear queue status file'),
1663 ('f', 'force', None, 'force copy')], 1913 ('f', 'force', None, 'force copy')],
1664 'hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]'), 1914 'hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]'),
1915 "qselect": (select,
1916 [('n', 'none', None, _('disable all guards')),
1917 ('s', 'series', None, _('list all guards in series file'))],
1918 'hg qselect [GUARDS]'),
1665 "qseries": 1919 "qseries":
1666 (series, 1920 (series,
1667 [('m', 'missing', None, 'print patches not in series'), 1921 [('m', 'missing', None, 'print patches not in series'),
1668 ('s', 'summary', None, _('print first line of patch header'))], 1922 ('s', 'summary', None, _('print first line of patch header'))],
1669 'hg qseries [-m]'), 1923 'hg qseries [-m]'),