diff --git a/hgext/mq.py b/hgext/mq.py --- a/hgext/mq.py +++ b/hgext/mq.py @@ -65,14 +65,17 @@ class queue: self.series_dirty = 0 self.series_path = "series" self.status_path = "status" + self.guards_path = "guards" + self.active_guards = None + self.guards_dirty = False if os.path.exists(self.join(self.series_path)): self.full_series = self.opener(self.series_path).read().splitlines() self.parse_series() if os.path.exists(self.join(self.status_path)): - self.applied = [statusentry(l) - for l in self.opener(self.status_path).read().splitlines()] + lines = self.opener(self.status_path).read().splitlines() + self.applied = [statusentry(l) for l in lines] def join(self, *p): return os.path.join(self.path, *p) @@ -90,12 +93,122 @@ class queue: index += 1 return None + guard_re = re.compile(r'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)') + def parse_series(self): self.series = [] + self.series_guards = [] for l in self.full_series: - s = l.split('#', 1)[0].strip() - if s: - self.series.append(s) + h = l.find('#') + if h == -1: + patch = l + comment = '' + elif h == 0: + continue + else: + patch = l[:h] + comment = l[h:] + patch = patch.strip() + if patch: + self.series.append(patch) + self.series_guards.append(self.guard_re.findall(comment)) + + def check_guard(self, guard): + bad_chars = '# \t\r\n\f' + first = guard[0] + for c in '-+': + if first == c: + return (_('guard %r starts with invalid character: %r') % + (guard, c)) + for c in bad_chars: + if c in guard: + return _('invalid character in guard %r: %r') % (guard, c) + + def set_active(self, guards): + for guard in guards: + bad = self.check_guard(guard) + if bad: + raise util.Abort(bad) + guards = dict.fromkeys(guards).keys() + guards.sort() + self.ui.debug('active guards: %s\n' % ' '.join(guards)) + self.active_guards = guards + self.guards_dirty = True + + def active(self): + if self.active_guards is None: + self.active_guards = [] + try: + guards = self.opener(self.guards_path).read().split() + except IOError, err: + if err.errno != errno.ENOENT: raise + guards = [] + for i, guard in enumerate(guards): + bad = self.check_guard(guard) + if bad: + self.ui.warn('%s:%d: %s\n' % + (self.join(self.guards_path), i + 1, bad)) + else: + self.active_guards.append(guard) + return self.active_guards + + def set_guards(self, idx, guards): + for g in guards: + if len(g) < 2: + raise util.Abort(_('guard %r too short') % g) + if g[0] not in '-+': + raise util.Abort(_('guard %r starts with invalid char') % g) + bad = self.check_guard(g[1:]) + if bad: + raise util.Abort(bad) + drop = self.guard_re.sub('', self.full_series[idx]) + self.full_series[idx] = drop + ''.join([' #' + g for g in guards]) + self.parse_series() + self.series_dirty = True + + def pushable(self, idx): + if isinstance(idx, str): + idx = self.series.index(idx) + patchguards = self.series_guards[idx] + if not patchguards: + return True, None + default = False + guards = self.active() + exactneg = [g for g in patchguards if g[0] == '-' and g[1:] in guards] + if exactneg: + return False, exactneg[0] + pos = [g for g in patchguards if g[0] == '+'] + exactpos = [g for g in pos if g[1:] in guards] + if pos: + if exactpos: + return True, exactpos[0] + return False, '' + return True, '' + + def explain_pushable(self, idx, all_patches=False): + write = all_patches and self.ui.write or self.ui.warn + if all_patches or self.ui.verbose: + if isinstance(idx, str): + idx = self.series.index(idx) + pushable, why = self.pushable(idx) + if all_patches and pushable: + if why is None: + write(_('allowing %s - no guards in effect\n') % + self.series[idx]) + else: + if not why: + write(_('allowing %s - no matching negative guards\n') % + self.series[idx]) + else: + write(_('allowing %s - guarded by %r\n') % + (self.series[idx], why)) + if not pushable: + if why and why[0] in '-+': + write(_('skipping %s - guarded by %r\n') % + (self.series[idx], why)) + else: + write(_('skipping %s - no matching guards\n') % + self.series[idx]) def save_dirty(self): def write_list(items, path): @@ -105,6 +218,7 @@ class queue: fp.close() if self.applied_dirty: write_list(map(str, self.applied), self.status_path) if self.series_dirty: write_list(self.full_series, self.series_path) + if self.guards_dirty: write_list(self.active_guards, self.guards_path) def readheaders(self, patch): def eatdiff(lines): @@ -257,7 +371,10 @@ class queue: if not patch: self.ui.warn("patch %s does not exist\n" % patch) return (1, None) - + pushable, reason = self.pushable(patch) + if not pushable: + self.explain_pushable(patch, all_patches=True) + continue info = mergeq.isapplied(patch) if not info: self.ui.warn("patch %s is not applied\n" % patch) @@ -321,6 +438,10 @@ class queue: tr = repo.transaction() n = None for patch in series: + pushable, reason = self.pushable(patch) + if not pushable: + self.explain_pushable(patch, all_patches=True) + continue self.ui.warn("applying %s\n" % patch) pf = os.path.join(patchdir, patch) @@ -639,8 +760,7 @@ class queue: pass else: if sno < len(self.series): - patch = self.series[sno] - return patch + return self.series[sno] if not strict: # return any partial match made above if res: @@ -926,18 +1046,26 @@ class queue: start = self.series_end() else: start = self.series.index(patch) + 1 - return [(i, self.series[i]) for i in xrange(start, len(self.series))] + unapplied = [] + for i in xrange(start, len(self.series)): + pushable, reason = self.pushable(i) + if pushable: + unapplied.append((i, self.series[i])) + self.explain_pushable(i) + return unapplied def qseries(self, repo, missing=None, summary=False): - start = self.series_end() + start = self.series_end(all_patches=True) if not missing: for i in range(len(self.series)): patch = self.series[i] if self.ui.verbose: if i < start: status = 'A' + elif self.pushable(i)[0]: + status = 'U' else: - status = 'U' + status = 'G' self.ui.write('%d %s ' % (i, status)) if summary: msg = self.readheaders(patch)[0] @@ -1060,16 +1188,27 @@ class queue: return end + 1 return 0 - def series_end(self): + def series_end(self, all_patches=False): end = 0 + def next(start): + if all_patches: + return start + i = start + while i < len(self.series): + p, reason = self.pushable(i) + if p: + break + self.explain_pushable(i) + i += 1 + return i if len(self.applied) > 0: p = self.applied[-1].name try: end = self.series.index(p) except ValueError: return 0 - return end + 1 - return end + return next(end + 1) + return next(end) def qapplied(self, repo, patch=None): if patch and patch not in self.series: @@ -1372,6 +1511,51 @@ def fold(ui, repo, *files, **opts): q.save_dirty() +def guard(ui, repo, *args, **opts): + '''set or print guards for a patch + + guards control whether a patch can be pushed. a patch with no + guards is aways pushed. a patch with posative guard ("+foo") is + pushed only if qselect command enables guard "foo". a patch with + nagative guard ("-foo") is never pushed if qselect command enables + guard "foo". + + with no arguments, default is to print current active guards. + with arguments, set active guards for patch. + + to set nagative guard "-foo" on topmost patch ("--" is needed so + hg will not interpret "-foo" as argument): + hg qguard -- -foo + + to set guards on other patch: + hg qguard other.patch +2.6.17 -stable + ''' + def status(idx): + guards = q.series_guards[idx] or ['unguarded'] + ui.write('%s: %s\n' % (q.series[idx], ' '.join(guards))) + q = repo.mq + patch = None + args = list(args) + if opts['list']: + if args or opts['none']: + raise util.Abort(_('cannot mix -l/--list with options or arguments')) + for i in xrange(len(q.series)): + status(i) + return + if not args or args[0][0:1] in '-+': + if not q.applied: + raise util.Abort(_('no patches applied')) + patch = q.applied[-1].name + if patch is None and args[0][0:1] not in '-+': + patch = args.pop(0) + if patch is None: + raise util.Abort(_('no patch to work with')) + if args or opts['none']: + q.set_guards(q.find_series(patch), args) + q.save_dirty() + else: + status(q.series.index(q.lookup(patch))) + def header(ui, repo, patch=None): """Print the header of the topmost or specified patch""" q = repo.mq @@ -1546,6 +1730,69 @@ def strip(ui, repo, rev, **opts): repo.mq.strip(repo, rev, backup=backup) return 0 +def select(ui, repo, *args, **opts): + '''set or print guarded patches to push + + use qguard command to set or print guards on patch. then use + qselect to tell mq which guards to use. example: + + qguard foo.patch -stable (nagative guard) + qguard bar.patch +stable (posative guard) + qselect stable + + this sets "stable" guard. mq will skip foo.patch (because it has + nagative match) but push bar.patch (because it has posative + match). + + with no arguments, default is to print current active guards. + with arguments, set active guards as given. + + use -n/--none to deactivate guards (no other arguments needed). + when no guards active, patches with posative guards are skipped, + patches with nagative guards are pushed. + + use -s/--series to print list of all guards in series file (no + other arguments needed). use -v for more information.''' + + q = repo.mq + guards = q.active() + if args or opts['none']: + q.set_active(args) + q.save_dirty() + if not args: + ui.status(_('guards deactivated\n')) + if q.series: + pushable = [p for p in q.unapplied(repo) if q.pushable(p[0])[0]] + ui.status(_('%d of %d unapplied patches active\n') % + (len(pushable), len(q.series))) + elif opts['series']: + guards = {} + noguards = 0 + for gs in q.series_guards: + if not gs: + noguards += 1 + for g in gs: + guards.setdefault(g, 0) + guards[g] += 1 + if ui.verbose: + guards['NONE'] = noguards + guards = guards.items() + guards.sort(lambda a, b: cmp(a[0][1:], b[0][1:])) + if guards: + ui.note(_('guards in series file:\n')) + for guard, count in guards: + ui.note('%2d ' % count) + ui.write(guard, '\n') + else: + ui.note(_('no guards in series file\n')) + else: + if guards: + ui.note(_('active guards:\n')) + for g in guards: + ui.write(g, '\n') + else: + ui.write(_('no active guards\n')) + def version(ui, q=None): """print the version number of the mq extension""" ui.write("mq version %s\n" % versionstr) @@ -1605,6 +1852,9 @@ cmdtable = { ('m', 'message', '', _('set patch header to ')), ('l', 'logfile', '', _('set patch header to contents of '))], 'hg qfold [-e] [-m ] [-l > $HGTMP/.hgrc +echo "mq=" >> $HGTMP/.hgrc + +hg init +hg qinit + +echo x > x +hg ci -Ama + +hg qnew a.patch +echo a > a +hg add a +hg qrefresh + +hg qnew b.patch +echo b > b +hg add b +hg qrefresh + +hg qnew c.patch +echo c > c +hg add c +hg qrefresh + +hg qpop -a + +echo % should fail +hg qguard +fail + +hg qpush +echo % should guard a.patch +hg qguard +a +echo % should print +a +hg qguard +hg qpop + +hg qguard a.patch +echo % should push b.patch +hg qpush + +hg qpop +hg qselect a +echo % should push a.patch +hg qpush + +hg qguard c.patch -a +echo % should print -a +hg qguard c.patch + +echo % should skip c.patch +hg qpush -a + +hg qguard -n c.patch +echo % should push c.patch +hg qpush -a + +hg qpop -a +hg qselect -n +hg qpush -a diff --git a/tests/test-mq.out b/tests/test-mq.out --- a/tests/test-mq.out +++ b/tests/test-mq.out @@ -30,6 +30,7 @@ list of commands (use "hg help -v mq" to qdelete remove a patch from the series file qdiff diff of the current patch qfold fold the named patches into the current patch + qguard set or print guards for a patch qheader Print the header of the topmost or specified patch qimport import a patch qinit init a new queue repository @@ -42,6 +43,7 @@ list of commands (use "hg help -v mq" to qrename rename a patch qrestore restore the queue state saved by a rev qsave save current queue state + qselect set or print guarded patches to push qseries print the entire series file qtop print the name of the current patch qunapplied print the patches not yet applied