Mercurial > hg
comparison hgext/mq.py @ 2821: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
2820:1bb8dd08c594 | 2821: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]'), |