This file is indexed.

/usr/share/pyshared/DITrack/Command/act.py is in ditrack 0.8-1.2.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  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
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
#
# act.py - DITrack 'act' command
#
# Copyright (c) 2006-2008 The DITrack Project, www.ditrack.org.
#
# $Id: act.py 2516 2008-05-26 14:25:52Z gli $
# $HeadURL: https://svn.xiolabs.com/ditrack/src/tags/0.8/DITrack/Command/act.py $
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#  * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#

import copy
import email
import email.Message
import os
import sys

# DITrack modules
import DITrack.Command.generic
import DITrack.Edit
import DITrack.UI
import DITrack.Util.common

class _InconsistencyError(Exception):
    """
    The action would create a database inconsistency.
    """

    def __init__(self, issue_id, message):
        self.issue_id = issue_id
        self.message = message

class _Driver:
    """
    Class representing a driver: en entity that generates actions to be 
    performed on the issue(s).
    """

    def __init__(self, globals, dbcfg, issues, future_versions, vsets):
        """
        Initializes the driver environment. Parameters are:

        DBCFG
            A database configuration object.

        FUTURE_VERSIONS
            A list of version strings that can be used as future versions. May
            be empty if the issues being dealt with don't have common future
            versions.

        GLOBALS
            Globals object.

        ISSUES
            A dictionary of issue objects. Keys are issue ids (strings).

        VSETS
            A list of version sets of the issues we deal with. No duplicates
            allowed.
        """

        self._dbcfg = dbcfg
        self._future_versions = future_versions
        self._globals = globals
        self._issues = issues
        self._vsets = vsets

        #
        # Helper data
        #

        self._single_issue = (len(self._issues) == 1)

        self._issue_numbers = self._issues.keys()
        self._issue_numbers.sort(lambda x,y: cmp(int(x), int(y)))

    def _change_issues_due_version(self, version):
        """
        Changes due version of all issues to VERSION.
        """
        for i in self._issues.values():
            i.change_due_version(version)

    def _close_issues(self, resolution):
        """
        Close all issues with specified RESOLUTION.
        """
        assert resolution

        for id in self._issues:
            try:
                self._issues[id].close(resolution)
            except DITrack.DB.Exceptions.InconsistentActionError, msg:
                raise _InconsistencyError(id, msg)

    def _reassign_issues(self, owner):
        """
        Reassign all issues to OWNER.
        """

        for i in self._issues.values():
            i.reassign(owner)

    def _reopen_issues(self):
        """
        Reopen all issues.
        """

        for id in self._issues:
            try:
                self._issues[id].reopen()
            except DITrack.DB.Exceptions.InconsistentActionError, msg:
                raise _InconsistencyError(id, msg)

    def _change_issues_header(self, header):
        """
        Update (or add) a single header of/to all issues. The header name and
        its new value are passed in HEADER, separated by '='.

        Raises ValueError if HEADER couldn't be parsed out.
        """
        k, v = header.split("=", 1)
        for issue in self._issues.itervalues():
            # XXX: should probably use internal method like replace_header().
            issue.info[k] = v

    def run(self):
        """
        Runs the driver. Returns a sorted list of issue numbers (XXX) to try
        saving. Empty list returned means "don't save the changes".
        """
        raise NotImplementedError

class _CmdlineDriver(_Driver):
    """
    Action driver for noninteractive (command line) sessions.
    """

    def __init__(self, actionlist, comment_text, **kv):
        """
        ACTIONLIST is a parameter to '-a' option, as described in the usage 
        note. COMMENT_TEXT is the comment text to add (may be "").

        The rest parameters are the same as the base class constructor method
        accepts.
        """
        _Driver.__init__(self, **kv)

        if actionlist:
            self._actions = actionlist.strip().split(",")
        else:
            self._actions = []

        self.comment_text = comment_text

    def _invalid_action(self, action, issue, msg):
        """
        Prints out diagnostic message MSG about ACTION on ISSUE (string) and
        returns an empty list.
        """

        DITrack.Util.common.err(
            "Can't do '%s' on i#%s: %s" % (action, issue, msg)
        )
        return []

    def run(self):

        for a in self._actions:
            a = a.split(":")
            if len(a) == 1:
                action, arg = a[0], None
            elif len(a) == 2:
                action, arg = a
            else:
                DITrack.Util.common.err("Invalid action list syntax")

            if action == "change-due":

                if (not arg) or (arg not in self._future_versions):
                    DITrack.Util.common.err(
                        "'change-due' requires a valid future version number "
                        "as the argument"
                    )

                self._change_issues_due_version(arg)

            elif action == "close":

                valid_resolutions = ("dropped", "fixed", "invalid")
                if (not arg) or (arg not in valid_resolutions):
                    DITrack.Util.common.err(
                        "'close' requires one of %s as the argument" %
                        ", ".join(["'%s'" % x for x in valid_resolutions])
                    )

                try:
                    self._close_issues(arg)
                except _InconsistencyError, e:
                    return self._invalid_action(action, e.issue_id, e.message)

            elif action == "reassign":
                # XXX: this check really belongs to the database layer
                if (not arg) or (arg not in self._dbcfg.users):
                    DITrack.Util.common.err(
                        "'reassign' requires a valid user name as the argument"
                    )

                self._reassign_issues(arg)

            elif action == "reopen":
                if arg:
                    DITrack.Util.common.err(
                        "'reopen' doesn't accept arguments"
                    )

                try:
                    self._reopen_issues()
                except _InconsistencyError, e:
                    return self._invalid_action(action, e.issue_id, e.message)

            elif action == "change-header":
                try:
                    self._change_issues_header(arg)
                except ValueError:
                    DITrack.Util.common.err(
                        "'change-header' requires 'header=value' argument"
                    )

            else:
                DITrack.Util.common.err("Invalid action: %s" % action)

        return self._issue_numbers

class _InteractiveDriver(_Driver):
    """
    Action driver for interactive sessions.
    """

    def _list_attaches(self, issue):
            attaches = issue.attachments()
            qty = len(attaches)
            sys.stdout.write(
                "\n%d file(s) currently attached\n" % qty
            )

            for i in range(qty):

                flags = ""
                if attaches[i].is_local:
                    flags = "L"

                sys.stdout.write(
                    "%3d %-3s %s\n" % (i + 1, flags, attaches[i].name)
                )

            sys.stdout.write("\n")


    def _manage_attaches(self, issue):
        mi_abort = DITrack.UI.MenuItem("a", "abandon this menu")
        mi_new = DITrack.UI.MenuItem("n", "new attach")
        mi_remove = DITrack.UI.MenuItem("r", "remove attach")

        menu = DITrack.UI.Menu(
            "Choose an action to manage attaches",
            [
                mi_abort,
                mi_new,
                mi_remove,
            ])

        while 1:
            # List the attaches
            self._list_attaches(issue)
            
            r = menu.run()

            if r == mi_abort:
                break
            
            elif r == mi_new:
                ti = DITrack.UI.TextInput("File to attach (blank to abort)")

                while 1:
                    fname = ti.run()
                    if not fname:
                        break

                    if not os.path.exists(fname):
                        sys.stdout.write("File doesn't exist: %s\n" % fname)
                        continue

                    if not os.path.isfile(fname):
                        sys.stdout.write("Not a file: %s\n" % fname)
                        continue

                    try:
                        issue.add_attachment(fname)
                        break
                    except ValueError, name:
                        sys.stdout.write(
                            "Attachment named '%s' already exists\n" % name
                        )
                    except DITrack.DB.Exceptions.BadAttachmentNameError, name:
                        sys.stdout.write(
                            "Attachment named '%s' has been removed within "
                            "this session; can't add another one with the "
                            "same name -- you need to save your changes first"
                            "\n"
                            % name
                        )

            elif r == mi_remove:
                # Create menu with a list of attachments
                removal_menu = DITrack.UI.EnumMenu(
                    "Choose an attachment to remove",
                    map(lambda x: x.name, issue.attachments()),
                    abort_option=True
                )

                fname = removal_menu.run()

                if fname is None:
                    continue

                issue.remove_attachment(fname)

                # Go straight to the main menu from here
                return

    def _reply_comment(self):

        assert self._single_issue

        # XXX: shouldn't a 'human-redable' representation of dates be a member
        # of the Comment class?
        def rm_timestamp(text):
            return text.split(" ", 1)[1]

        issue = self._issues[self._issue_numbers[0]]

        # Get only "firm" comments
        comments = issue.comments(local=False)

        # Creating comment choice menu
        mi_reply_abort = DITrack.UI.MenuItem("a", "abort")

        mi_reply_comments = [
            DITrack.UI.MenuItem(
                0,
                "original description by %s, %s" % (
                    issue.info["Opened-by"],
                    rm_timestamp(issue.info["Opened-on"])
                )
            )
        ]

        for (id, c) in comments[1:]:
            mi_reply_comments.append(
                DITrack.UI.MenuItem(
                    int(id),
                    "comment #%s by %s, %s" % (
                        id, c.added_by, rm_timestamp(c.added_on)
                    )
                )
            )

        reply_menu = DITrack.UI.Menu(
            "Choose a comment to reply to",
            [mi_reply_abort] + mi_reply_comments
        )

        comment_id = None

        while comment_id is None:
            r = reply_menu.run()

            if r == mi_reply_abort:
                break
            else:
                # Comment id to reply to
                id = "%d" % r.key          
                c = issue[id]

                sys.stdout.write(
                    "\n======\n"
                    "\nComment #%s by %s, %s\n\n" % (
                        id,
                        c.added_by,
                        rm_timestamp(c.added_on)
                    )
                )
                sys.stdout.write("".join(c.header_as_strings()))
                sys.stdout.write("\n" + c.text + "\n======\n")

                ti_confirmation = DITrack.UI.TextInput(
                    "Is this the comment you'd like to reply to (y/n)?"
                )

                while 1:
                    choice = ti_confirmation.run()
                    if choice == "y":
                        comment_id = id
                        break
                    elif choice == "n":
                        break

        if comment_id is not None:
            c = issue[comment_id]

            def _quote_string(str):
                if str and (str[0] != ">"):
                    return "> " + str
                else:
                    return ">" + str

            self.comment_text = DITrack.Edit.edit_text(
                self._globals,
                "\nQuoting c#%s by %s, %s\n\n%s" % (
                    id,
                    c.added_by,
                    rm_timestamp(c.added_on),
                    "\n".join(map(_quote_string, c.text.split("\n")))
                )
            )

    def run(self):
        """
        If changes are to be saved (see the base class method description),
        the COMMENT_TEXT member contains the comment text for the action upon
        the return from this method.
        """

        # Build up the menu.
        mi_abort = DITrack.UI.MenuItem("a", "abort, discarding changes")
        mi_attaches = DITrack.UI.MenuItem("f", "manage file attaches")
        mi_ch_due_in = DITrack.UI.MenuItem("d", "change due version")
        mi_close = DITrack.UI.MenuItem("c", "close the issue")
        mi_edit_info = DITrack.UI.MenuItem("h", "edit the issue header")
        mi_edit_text = DITrack.UI.MenuItem("e", "edit comment text")
        mi_quit = DITrack.UI.MenuItem("q", "quit, saving changes")
        mi_reassign = DITrack.UI.MenuItem("o", "reassign the issue owner")
        mi_reopen = DITrack.UI.MenuItem("r", "reopen the issue")
        mi_reply = DITrack.UI.MenuItem("re", "reply to a comment")

        menu = DITrack.UI.Menu("Choose an action for the issue(s)",
            [
            mi_abort,
            mi_attaches,
            mi_ch_due_in,
            mi_close,
            mi_edit_info,
            mi_edit_text,
            mi_quit,
            mi_reassign,
            mi_reopen,
            mi_reply
            ])

        save_changes = False

        self.comment_text = ""

        mi_ch_due_in.enabled = self._future_versions
        mi_attaches.enabled = mi_edit_info.enabled = self._single_issue

        while 1:

            # Conditionally enable/disable menu items.
            mi_close.enabled = filter(lambda x: x.info["Status"] == "open",
                self._issues.itervalues())

            mi_reopen.enabled = filter(
                lambda x: x.info["Status"] == "closed",
                self._issues.itervalues()
            )

            mi_reply.enabled = self._single_issue and self.comment_text == ""

            sys.stdout.write("\nActing on:\n")

            output = dict([(x, "") for x in self._vsets])
            for id in self._issue_numbers:
                vset = self._dbcfg.category[
                    self._issues[id].info["Category"]
                ].version_set

                output[vset] += "i#%s: %s\n" % (
                    id, self._issues[id].info["Title"]
                )

            sys.stdout.write("\n")
            for vset in self._vsets:
                sys.stdout.write("[%s]:\n%s\n" % (vset, output[vset]))


            sys.stdout.write("\n")

            r = menu.run()
            if r == mi_abort:
                break

            elif r == mi_attaches:

                assert len(self._issue_numbers) == 1

                self._manage_attaches(self._issues[self._issue_numbers[0]])

            elif r == mi_close:

                mi_c_abort = DITrack.UI.MenuItem("a", "abort closing")
                mi_c_dropped = DITrack.UI.MenuItem("d", "dropped")
                mi_c_fixed = DITrack.UI.MenuItem("f", "fixed")
                mi_c_invalid = DITrack.UI.MenuItem("i", "invalid")

                resolution_menu = DITrack.UI.Menu(
                    "Choose the resolution",
                    [
                        mi_c_abort,
                        mi_c_dropped,
                        mi_c_fixed,
                        mi_c_invalid
                    ])

                r = resolution_menu.run()

                if r != mi_c_abort:
                    if r == mi_c_dropped:
                        resolution = "dropped"
                    elif r == mi_c_fixed:
                        resolution = "fixed"
                    elif r == mi_c_invalid:
                        resolution = "invalid"

                    self._close_issues(resolution)

            elif r == mi_ch_due_in:
                assert self._future_versions

                any_issue = self._issues[self._issue_numbers[0]]
                if self._single_issue:
                    sys.stdout.write("Current due version: %s\n" %
                        any_issue.info["Due-in"])

                due_menu = DITrack.UI.EnumMenu("Choose new due version", 
                        self._future_versions)

                v = due_menu.run()

                if not v: break

                self._change_issues_due_version(v)

            elif r == mi_edit_info:
                id = self._issue_numbers[0]

                info = email.Message.Message()

                keys = self._issues[id].info.keys()
                keys.sort()
                for k in keys:
                    info.add_header(k, self._issues[id].info[k])

                header = DITrack.Edit.edit_text(
                    self._globals, info.as_string()
                )

                self._issues[id].info = {}
                new_info = email.message_from_string(header)
                for h in new_info.keys():
                    self._issues[id].info[h] = new_info[h]

            elif r == mi_edit_text:
                self.comment_text = DITrack.Edit.edit_text(self._globals,
                    self.comment_text
                )

            elif r == mi_quit:
                save_changes = True
                break

            elif r == mi_reassign:

                if self._single_issue:
                    sys.stdout.write("Current issue owner: %s\n" %
                        self._issues[self._issue_numbers[0]].info["Owned-by"]
                    )

                users = self._dbcfg.users.keys()
                users.sort()
                owner_menu = DITrack.UI.EnumMenu("Choose new issue owner",
                    users)

                v = owner_menu.run()

                if not v: break

                self._reassign_issues(v)

            elif r == mi_reopen:
                self._reopen_issues()

            elif r == mi_reply:
                self._reply_comment()

            else:
                raise NotImplementedError

        if save_changes:
            return self._issue_numbers
        else:
            return []


class Handler(DITrack.Command.generic.Handler):
    canonical_name = "act"

    # XXX: replace ISSUENUM with ISSUEID later
    description = """Perform actions on an issue (or multiple issues).
usage: %s ISSUENUM [ISSUENUM...]""" % canonical_name

    description_ps = """
ACTIONLIST is a comma-separated list of one of more of the following actions:

    change-header:HEADER=VALUE
        - add/update issue(s) header HEADER with specified value VALUE;
        omitting VALUE removes the header.

    close:{dropped, fixed, invalid}
        - close the issue(s) with specified resolution.

    change-due:VERSION
        - change the due version to VERSION.

    reassign:USER
        - reassign the issue(s) to USER.

    reopen
        - reopen the issue(s).

Each action can occur in the ACTIONLIST once at the most.

Any of -a, -F or -m implies non-interactive mode.

-F and -m are mutually exclusive.
"""

    def run(self, opts, globals):
        self.check_options(opts)

        if len(opts.fixed) < 2:
            self.print_help(globals)
            sys.exit(1)

        # We'll need an editor.
        globals.get_editor()

        db = DITrack.Util.common.open_db(globals, opts, "w")

        present_vsets = {}

        issue = {}
        prev_issue = {}
        for id in opts.fixed[1:]:

            id = id.upper()
            try:
                issue[id] = db.issue_by_id(id)
            except (KeyError, ValueError):
                # Diagnostics printed by issue_by_id().
                pass

            if db.is_valid_issue_name(id):
                DITrack.Util.common.err(
                    "Non-local identifier expected '%s'" % id
                )

            prev_issue[id] = copy.deepcopy(issue[id])

            present_vsets[
                db.cfg.category[
                issue[id].info["Category"]
                ].version_set] = 1

        # Scan through version sets we are dealing with and find an 
        # intersection of all future versions.
        i = 0
        common_versions = []

        for vset in present_vsets:
            if not i:
                common_versions = db.cfg.versions[vset].future
                i += 1
            else:
                intersected_versions = []
                for version in db.cfg.versions[vset].future:
                    if version in common_versions:
                        intersected_versions.append(version)
                if len(intersected_versions) == 0:
                    break
                common_versions = intersected_versions
        
        common_versions.sort()

        if ("actions" in opts.var) or ("comment_file" in opts.var) or \
            ("comment_message" in opts.var):

            if "actions" in opts.var:
                actions = opts.var["actions"]
            else:
                actions = ""

            if "comment_file" in opts.var:
                comment_text = open(opts.var["comment_file"]).read()
            elif "comment_message" in opts.var:
                comment_text = "%s\n" % opts.var["comment_message"]
            else:
                comment_text = ""

            driver = _CmdlineDriver(
                actions,
                comment_text,
                globals=globals,
                dbcfg=db.cfg,
                issues=issue,
                future_versions=common_versions,
                vsets=present_vsets.keys()
            )
        else:
            driver = _InteractiveDriver(
                globals,
                db.cfg,
                issues=issue,
                future_versions=common_versions,
                vsets=present_vsets.keys()
            )

        issue_numbers = driver.run()

        if issue_numbers:

            # We need to save the changes

            local_names = []

            for id in issue_numbers:

                try:
                    name, comment = db.new_comment(id, prev_issue[id],
                        issue[id], driver.comment_text, globals.username,
                        globals.fmt_timestamp())

                    local_names.append((id, name))

                # XXX: should embrace only db.new_comment()
                except DITrack.DB.Exceptions.NoDifferenceCondition:
                    continue

                sys.stdout.write("Comment %s added to issue %s\n" % (name, id))

            if not opts.var["no_commits"]:
                # Now commit newly added comments. We do it in a separate step
                # to simplify the solution for now. If something goes wrong
                # (like no disk space or connectivity issues), a user may
                # choose to commit the changes later.

                for issue_id, comment_name in local_names:

                    # XXX: for now we don't deal with commenting local issues.
                    assert db.is_valid_issue_number(issue_id)

                    firm_id = db.commit_comment(issue_id, comment_name)

                    # XXX: print 'Local ... in r234'.
                    sys.stdout.write("Local i#%s.%s committed as i#%s.%s\n" % \
                        (issue_id, comment_name, issue_id, firm_id))