{"thread":{"id":"8222","subject":"[StGIT PATCH] Refactor message printing","startedAt":"2007-05-19T11:54:14Z","lastAt":"2007-05-19T11:54:14Z","messageCount":1,"participants":["Karl Hasselström"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"42616","messageId":"20070519115334.1611.17371.stgit@yoghurt","threadId":"8222","inReplyTo":null,"subject":"[StGIT PATCH] Refactor message printing","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2007-05-19T11:54:14Z","receivedAt":"2007-05-19T11:54:14Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Feed all those little progress and status messages through a central\nplace where such things as newlines, indentation, and suppressing\noutput when not on a TTY can be taken care of once and for all.\n\nThis patch takes care of almost, but not quite, all output. The most\nnotable leftover is the printing of help messages.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n---\n\n stgit/commands/applied.py    |    4 +-\n stgit/commands/assimilate.py |    6 +--\n stgit/commands/branch.py     |   55 +++++++++++++--------------\n stgit/commands/clean.py      |    7 +--\n stgit/commands/commit.py     |    5 +-\n stgit/commands/common.py     |   48 ++++++++++--------------\n stgit/commands/delete.py     |    2 -\n stgit/commands/diff.py       |    2 -\n stgit/commands/export.py     |    8 ++--\n stgit/commands/files.py      |    6 +--\n stgit/commands/fold.py       |    7 +--\n stgit/commands/hide.py       |    2 -\n stgit/commands/id.py         |    2 -\n stgit/commands/imprt.py      |   10 +----\n stgit/commands/log.py        |    2 -\n stgit/commands/mail.py       |   14 +++----\n stgit/commands/patches.py    |    2 -\n stgit/commands/pick.py       |   31 +++++++--------\n stgit/commands/pull.py       |    4 +-\n stgit/commands/push.py       |    7 +--\n stgit/commands/refresh.py    |   14 +++----\n stgit/commands/rename.py     |    5 +-\n stgit/commands/series.py     |    8 ++--\n stgit/commands/sync.py       |   14 ++-----\n stgit/commands/top.py        |    2 -\n stgit/commands/unapplied.py  |    4 +-\n stgit/commands/uncommit.py   |    8 +---\n stgit/commands/unhide.py     |    2 -\n stgit/git.py                 |   18 ++++-----\n stgit/gitmergeonefile.py     |   45 ++++++++++------------\n stgit/main.py                |   15 +++----\n stgit/stack.py               |   36 +++++++++---------\n stgit/utils.py               |   85 +++++++++++++++++++++++++++++++++++++++++-\n 33 files changed, 259 insertions(+), 221 deletions(-)\n\ndiff --git a/stgit/commands/applied.py b/stgit/commands/applied.py\nindex f131d62..0925de0 100644\n--- a/stgit/commands/applied.py\n+++ b/stgit/commands/applied.py\n@@ -47,7 +47,7 @@ def func(parser, options, args):\n     applied = crt_series.get_applied()\n \n     if options.count:\n-        print len(applied)\n+        out.stdout(len(applied))\n     else:\n         for p in applied:\n-            print p\n+            out.stdout(p)\ndiff --git a/stgit/commands/assimilate.py b/stgit/commands/assimilate.py\nindex a8b3bfe..e5ebb55 100644\n--- a/stgit/commands/assimilate.py\n+++ b/stgit/commands/assimilate.py\n@@ -42,7 +42,7 @@ def func(parser, options, args):\n     \"\"\"\n \n     def nothing_to_do():\n-        print 'No commits to assimilate'\n+        out.info('No commits to assimilate')\n \n     top_patch = crt_series.get_current_patch()\n     if not top_patch:\n@@ -79,8 +79,8 @@ def func(parser, options, args):\n \n     victims.reverse()\n     for victim in victims:\n-        print ('Creating patch \"%s\" from commit %s'\n-               % (patch2name[victim], victim))\n+        out.info('Creating patch \"%s\" from commit %s'\n+                 % (patch2name[victim], victim))\n         aname, amail, adate = name_email_date(victim.get_author())\n         cname, cmail, cdate = name_email_date(victim.get_committer())\n         crt_series.new_patch(\ndiff --git a/stgit/commands/branch.py b/stgit/commands/branch.py\nindex 5e7b0df..07bdca8 100644\n--- a/stgit/commands/branch.py\n+++ b/stgit/commands/branch.py\n@@ -83,8 +83,8 @@ def __print_branch(branch_name, length):\n         current = '>'\n     if branch.get_protected():\n         protected = 'p'\n-    print current + ' ' + initialized + protected + '\\t' + \\\n-          branch_name.ljust(length) + '  | ' + branch.get_description()\n+    out.stdout(current + ' ' + initialized + protected + '\\t'\n+               + branch_name.ljust(length) + '  | ' + branch.get_description())\n \n def __delete_branch(doomed_name, force = False):\n     doomed = stack.Series(doomed_name)\n@@ -92,8 +92,7 @@ def __delete_branch(doomed_name, force = False):\n     if doomed.get_protected():\n         raise CmdException, 'This branch is protected. Delete is not permitted'\n \n-    print 'Deleting branch \"%s\"...' % doomed_name,\n-    sys.stdout.flush()\n+    out.start('Deleting branch \"%s\"' % doomed_name)\n \n     if __is_current_branch(doomed_name):\n         check_local_changes()\n@@ -108,7 +107,7 @@ def __delete_branch(doomed_name, force = False):\n     if doomed_name != 'master':\n         git.delete_branch(doomed_name)\n \n-    print 'done'\n+    out.done()\n \n def func(parser, options, args):\n \n@@ -127,17 +126,20 @@ def func(parser, options, args):\n                 if git.rev_parse(args[1]) == git.rev_parse('refs/heads/' + args[1]):\n                     # we are for sure referring to a branch\n                     parentbranch = 'refs/heads/' + args[1]\n-                    print 'Recording \"%s\" as parent branch.' % parentbranch\n+                    out.info('Recording \"%s\" as parent branch' % parentbranch)\n                 elif git.rev_parse(args[1]) and re.search('/', args[1]):\n                     # FIXME: should the test be more strict ?\n                     parentbranch = args[1]\n                 else:\n                     # Note: this includes refs to StGIT patches\n-                    print 'Don\\'t know how to determine parent branch from \"%s\".' % args[1]\n+                    out.info('Don\\'t know how to determine parent branch'\n+                             ' from \"%s\"' % args[1])\n                     parentbranch = None\n             except git.GitException:\n-                # should use a more specific exception to catch only non-git refs ?\n-                print 'Don\\'t know how to determine parent branch from \"%s\".' % args[1]\n+                # should use a more specific exception to catch only\n+                # non-git refs ?\n+                out.info('Don\\'t know how to determine parent branch'\n+                         ' from \"%s\"' % args[1])\n                 parentbranch = None\n \n             tree_id = git_id(args[1])\n@@ -148,9 +150,10 @@ def func(parser, options, args):\n         if parentbranch:\n             parentremote = git.identify_remote(parentbranch)\n             if parentremote:\n-                print 'Using \"%s\" remote to pull parent from.' % parentremote\n+                out.info('Using remote \"%s\" to pull parent from'\n+                         % parentremote)\n             else:\n-                print 'Recording as a local branch.'\n+                out.info('Recording as a local branch')\n         else:\n             # no known parent branch, can't guess the remote\n             parentremote = None\n@@ -159,7 +162,7 @@ def func(parser, options, args):\n                                    parent_remote = parentremote,\n                                    parent_branch = parentbranch)\n \n-        print 'Branch \"%s\" created.' % args[0]\n+        out.info('Branch \"%s\" created' % args[0])\n         return\n \n     elif options.clone:\n@@ -176,10 +179,9 @@ def func(parser, options, args):\n         check_conflicts()\n         check_head_top_equal()\n \n-        print 'Cloning current branch to \"%s\"...' % clone,\n-        sys.stdout.flush()\n+        out.start('Cloning current branch to \"%s\"' % clone)\n         crt_series.clone(clone)\n-        print 'done'\n+        out.done()\n \n         return\n \n@@ -202,12 +204,12 @@ def func(parser, options, args):\n         branches.sort()\n \n         if branches:\n-            print 'Available branches:'\n+            out.info('Available branches:')\n             max_len = max([len(i) for i in branches])\n             for i in branches:\n                 __print_branch(i, max_len)\n         else:\n-            print 'No branches'\n+            out.info('No branches')\n         return\n \n     elif options.protect:\n@@ -224,10 +226,9 @@ def func(parser, options, args):\n             raise CmdException, 'Branch \"%s\" is not controlled by StGIT' \\\n                   % branch_name\n \n-        print 'Protecting branch \"%s\"...' % branch_name,\n-        sys.stdout.flush()\n+        out.start('Protecting branch \"%s\"' % branch_name)\n         branch.protect()\n-        print 'done'\n+        out.done()\n \n         return\n \n@@ -241,7 +242,7 @@ def func(parser, options, args):\n \n         stack.Series(args[0]).rename(args[1])\n \n-        print 'Renamed branch \"%s\" as \"%s\".' % (args[0], args[1])\n+        out.info('Renamed branch \"%s\" to \"%s\"' % (args[0], args[1]))\n \n         return\n \n@@ -259,10 +260,9 @@ def func(parser, options, args):\n             raise CmdException, 'Branch \"%s\" is not controlled by StGIT' \\\n                   % branch_name\n \n-        print 'Unprotecting branch \"%s\"...' % branch_name,\n-        sys.stdout.flush()\n+        out.info('Unprotecting branch \"%s\"' % branch_name)\n         branch.unprotect()\n-        print 'done'\n+        out.done()\n \n         return\n \n@@ -294,12 +294,9 @@ def func(parser, options, args):\n         check_conflicts()\n         check_head_top_equal()\n \n-        print 'Switching to branch \"%s\"...' % args[0],\n-        sys.stdout.flush()\n-\n+        out.start('Switching to branch \"%s\"' % args[0])\n         git.switch_branch(args[0])\n-\n-        print 'done'\n+        out.done()\n         return\n \n     # default action: print the current branch\ndiff --git a/stgit/commands/clean.py b/stgit/commands/clean.py\nindex 8f5c606..7b57526 100644\n--- a/stgit/commands/clean.py\n+++ b/stgit/commands/clean.py\n@@ -43,14 +43,11 @@ def __delete_empty(patches, applied):\n     \"\"\"\n     for p in patches:\n         if crt_series.empty_patch(p):\n-            print 'Deleting patch \"%s\"...' % p,\n-            sys.stdout.flush()\n-\n+            out.start('Deleting patch \"%s\"' % p)\n             if applied and crt_series.patch_applied(p):\n                 crt_series.pop_patch(p)\n             crt_series.delete_patch(p)\n-\n-            print 'done'\n+            out.done()\n         elif applied and crt_series.patch_unapplied(p):\n             crt_series.push_patch(p)\n \ndiff --git a/stgit/commands/commit.py b/stgit/commands/commit.py\nindex 8af6651..2b8d7ce 100644\n--- a/stgit/commands/commit.py\n+++ b/stgit/commands/commit.py\n@@ -54,8 +54,7 @@ def func(parser, options, args):\n \n     crt_head = git.get_head()\n \n-    print 'Committing %d patches...' % len(applied),\n-    sys.stdout.flush()\n+    out.start('Committing %d patches' % len(applied))\n \n     crt_series.pop_patch(applied[0])\n     git.switch(crt_head)\n@@ -63,4 +62,4 @@ def func(parser, options, args):\n     for patch in applied:\n         crt_series.delete_patch(patch)\n \n-    print 'done'\n+    out.done()\ndiff --git a/stgit/commands/common.py b/stgit/commands/common.py\nindex 28026da..22c78ae 100644\n--- a/stgit/commands/common.py\n+++ b/stgit/commands/common.py\n@@ -131,9 +131,9 @@ def print_crt_patch(branch = None):\n         patch = stack.Series(branch).get_current()\n \n     if patch:\n-        print 'Now at patch \"%s\"' % patch\n+        out.info('Now at patch \"%s\"' % patch)\n     else:\n-        print 'No patches applied'\n+        out.info('No patches applied')\n \n def resolved(filename, reset = None):\n     if reset:\n@@ -163,58 +163,53 @@ def push_patches(patches, check_merged = False):\n     \"\"\"\n     forwarded = crt_series.forward_patches(patches)\n     if forwarded > 1:\n-        print 'Fast-forwarded patches \"%s\" - \"%s\"' % (patches[0],\n-                                                      patches[forwarded - 1])\n+        out.info('Fast-forwarded patches \"%s\" - \"%s\"'\n+                 % (patches[0], patches[forwarded - 1]))\n     elif forwarded == 1:\n-        print 'Fast-forwarded patch \"%s\"' % patches[0]\n+        out.info('Fast-forwarded patch \"%s\"' % patches[0])\n \n     names = patches[forwarded:]\n \n     # check for patches merged upstream\n     if names and check_merged:\n-        print 'Checking for patches merged upstream...',\n-        sys.stdout.flush()\n+        out.start('Checking for patches merged upstream')\n \n         merged = crt_series.merged_patches(names)\n \n-        print 'done (%d found)' % len(merged)\n+        out.done('%d found' % len(merged))\n     else:\n         merged = []\n \n     for p in names:\n-        print 'Pushing patch \"%s\"...' % p,\n-        sys.stdout.flush()\n+        out.start('Pushing patch \"%s\"' % p)\n \n         if p in merged:\n             crt_series.push_patch(p, empty = True)\n-            print 'done (merged upstream)'\n+            out.done('merged upstream')\n         else:\n             modified = crt_series.push_patch(p)\n \n             if crt_series.empty_patch(p):\n-                print 'done (empty patch)'\n+                out.done('empty patch')\n             elif modified:\n-                print 'done (modified)'\n+                out.done('modified')\n             else:\n-                print 'done'\n+                out.done()\n \n def pop_patches(patches, keep = False):\n     \"\"\"Pop the patches in the list from the stack. It is assumed that\n     the patches are listed in the stack reverse order.\n     \"\"\"\n     if len(patches) == 0:\n-        print 'nothing to push/pop'\n+        out.info('Nothing to push/pop')\n     else:\n         p = patches[-1]\n         if len(patches) == 1:\n-            print 'Popping patch \"%s\"...' % p,\n+            out.start('Popping patch \"%s\"' % p)\n         else:\n-            print 'Popping \"%s\" - \"%s\" patches...' % (patches[0], p),\n-        sys.stdout.flush()\n-\n+            out.start('Popping patches \"%s\" - \"%s\"' % (patches[0], p))\n         crt_series.pop_patch(p, keep)\n-\n-        print 'done'\n+        out.done()\n \n def parse_patches(patch_args, patch_list, boundary = 0, ordered = False):\n     \"\"\"Parse patch_args list for patch names in patch_list and return\n@@ -334,19 +329,18 @@ def prepare_rebase(real_rebase, force=None):\n     # pop all patches\n     applied = crt_series.get_applied()\n     if len(applied) > 0:\n-        print 'Popping all applied patches...',\n-        sys.stdout.flush()\n+        out.start('Popping all applied patches')\n         crt_series.pop_patch(applied[0])\n-        print 'done'\n+        out.done()\n     return applied\n \n def rebase(target):\n     if target == git.get_head():\n-        print 'Already at \"%s\", no need for rebasing.' % target\n+        out.info('Already at \"%s\", no need for rebasing.' % target)\n         return\n-    \n-    print 'Rebasing to \"%s\"...' % target\n+    out.start('Rebasing to \"%s\"' % target)\n     git.reset(tree_id = git_id(target))\n+    out.done()\n \n def post_rebase(applied, nopush, merged):\n     # memorize that we rebased to here\ndiff --git a/stgit/commands/delete.py b/stgit/commands/delete.py\nindex e1a70c9..a9e2744 100644\n--- a/stgit/commands/delete.py\n+++ b/stgit/commands/delete.py\n@@ -74,7 +74,7 @@ def func(parser, options, args):\n     # delete the patches\n     for patch in applied + patches:\n         crt_series.delete_patch(patch)\n-        print 'Patch \"%s\" successfully deleted' % patch\n+        out.info('Patch \"%s\" successfully deleted' % patch)\n \n     if not options.branch:\n         print_crt_patch()\ndiff --git a/stgit/commands/diff.py b/stgit/commands/diff.py\nindex d3e1190..f56cbeb 100644\n--- a/stgit/commands/diff.py\n+++ b/stgit/commands/diff.py\n@@ -80,7 +80,7 @@ def func(parser, options, args):\n         rev2 = None\n \n     if options.stat:\n-        print git.diffstat(args, git_id(rev1), git_id(rev2))\n+        out.stdout_raw(git.diffstat(args, git_id(rev1), git_id(rev2)) + '\\n')\n     else:\n         diff_str = git.diff(args, git_id(rev1), git_id(rev2),\n                             binary = options.binary)\ndiff --git a/stgit/commands/export.py b/stgit/commands/export.py\nindex 20d8f67..cafcbe3 100644\n--- a/stgit/commands/export.py\n+++ b/stgit/commands/export.py\n@@ -79,8 +79,8 @@ def func(parser, options, args):\n         dirname = 'patches-%s' % crt_series.get_branch()\n \n     if not options.branch and git.local_changes():\n-        print 'Warning: local changes in the tree. ' \\\n-              'You might want to commit them first'\n+        out.warn('Local changes in the tree;'\n+                 ' you might want to commit them first')\n \n     if not options.stdout:\n         if not os.path.isdir(dirname):\n@@ -166,9 +166,9 @@ def func(parser, options, args):\n             f = open(pfile, 'w+')\n \n         if options.stdout and num > 1:\n-            print '-------------------------------------------------------------------------------'\n+            print '-'*79\n             print patch.get_name()\n-            print '-------------------------------------------------------------------------------'\n+            print '-'*79\n \n         # write description\n         f.write(descr)\ndiff --git a/stgit/commands/files.py b/stgit/commands/files.py\nindex b33bd2a..59893d8 100644\n--- a/stgit/commands/files.py\n+++ b/stgit/commands/files.py\n@@ -57,8 +57,8 @@ def func(parser, options, args):\n     rev2 = git_id('%s//top' % patch)\n \n     if options.stat:\n-        print git.diffstat(rev1 = rev1, rev2 = rev2)\n+        out.stdout_raw(git.diffstat(rev1 = rev1, rev2 = rev2) + '\\n')\n     elif options.bare:\n-        print git.barefiles(rev1, rev2)\n+        out.stdout_raw(git.barefiles(rev1, rev2) + '\\n')\n     else:\n-        print git.files(rev1, rev2)\n+        out.stdout_raw(git.files(rev1, rev2) + '\\n')\ndiff --git a/stgit/commands/fold.py b/stgit/commands/fold.py\nindex e87ded2..297dfbf 100644\n--- a/stgit/commands/fold.py\n+++ b/stgit/commands/fold.py\n@@ -61,12 +61,11 @@ def func(parser, options, args):\n \n     if filename:\n         if os.path.exists(filename):\n-            print 'Folding patch \"%s\"...' % filename,\n+            out.start('Folding patch \"%s\"' % filename)\n         else:\n             raise CmdException, 'No such file: %s' % filename\n     else:\n-        print 'Folding patch from stdin...',\n-    sys.stdout.flush()\n+        out.start('Folding patch from stdin')\n \n     if options.threeway:\n         crt_patch = crt_series.get_patch(current)\n@@ -77,4 +76,4 @@ def func(parser, options, args):\n     else:\n         git.apply_patch(filename = filename)\n \n-    print 'done'\n+    out.done()\ndiff --git a/stgit/commands/hide.py b/stgit/commands/hide.py\nindex 9ef948d..3cb08e8 100644\n--- a/stgit/commands/hide.py\n+++ b/stgit/commands/hide.py\n@@ -46,4 +46,4 @@ def func(parser, options, args):\n \n     for patch in patches:\n         crt_series.hide_patch(patch)\n-        print 'Patch \"%s\" hidden' % patch\n+        out.info('Patch \"%s\" hidden' % patch)\ndiff --git a/stgit/commands/id.py b/stgit/commands/id.py\nindex 4226adf..8c717e8 100644\n--- a/stgit/commands/id.py\n+++ b/stgit/commands/id.py\n@@ -46,4 +46,4 @@ def func(parser, options, args):\n     else:\n         parser.error('incorrect number of arguments')\n \n-    print git_id(id_str)\n+    out.stdout(git_id(id_str))\ndiff --git a/stgit/commands/imprt.py b/stgit/commands/imprt.py\nindex 6fcdc62..0089a8b 100644\n--- a/stgit/commands/imprt.py\n+++ b/stgit/commands/imprt.py\n@@ -255,7 +255,7 @@ def __create_patch(filename, message, author_name, author_email,\n         raise CmdException, 'No diff found inside the patch'\n \n     if options.ignore and patch in crt_series.get_applied():\n-        print 'Ignoring already applied patch \"%s\"' % patch\n+        out.info('Ignoring already applied patch \"%s\"' % patch)\n         return\n     if options.replace and patch in crt_series.get_unapplied():\n         crt_series.delete_patch(patch)\n@@ -289,18 +289,14 @@ def __create_patch(filename, message, author_name, author_email,\n                          committer_name = committer_name,\n                          committer_email = committer_email)\n \n-    print 'Importing patch \"%s\"...' % patch,\n-    sys.stdout.flush()\n-\n+    out.start('Importing patch \"%s\"' % patch)\n     if options.base:\n         git.apply_patch(diff = diff, base = git_id(options.base))\n     else:\n         git.apply_patch(diff = diff)\n-\n     crt_series.refresh_patch(edit = options.edit,\n                              show_patch = options.showpatch)\n-\n-    print 'done'    \n+    out.done()\n \n def __import_file(filename, options, patch = None):\n     \"\"\"Import a patch from a file or standard input\ndiff --git a/stgit/commands/log.py b/stgit/commands/log.py\nindex a21789e..e3e17f9 100644\n--- a/stgit/commands/log.py\n+++ b/stgit/commands/log.py\n@@ -70,7 +70,7 @@ def show_log(log, show_patch):\n             secs, tz = author_date.split()\n             date = '%s %s' % (time.ctime(int(secs)), tz)\n \n-            print descr, date\n+            out.stdout('%s %s' % (descr, date))\n \n         parent = commit.get_parent()\n         if parent:\ndiff --git a/stgit/commands/mail.py b/stgit/commands/mail.py\nindex 2fcaa5f..1ca6ba2 100644\n--- a/stgit/commands/mail.py\n+++ b/stgit/commands/mail.py\n@@ -500,13 +500,12 @@ def func(parser, options, args):\n             ref_id = msg_id\n \n         if options.mbox:\n-            print msg_string\n+            out.stdout_raw(msg_string + '\\n')\n         else:\n-            print 'Sending the cover message...',\n-            sys.stdout.flush()\n+            out.start('Sending the cover message')\n             __send_message(smtpserver, from_addr, to_addr_list, msg_string,\n                            sleep, smtpuser, smtppassword)\n-            print 'done'\n+            out.done()\n \n     # send the patches\n     if options.template:\n@@ -529,10 +528,9 @@ def func(parser, options, args):\n             ref_id = msg_id\n \n         if options.mbox:\n-            print msg_string\n+            out.stdout_raw(msg_string + '\\n')\n         else:\n-            print 'Sending patch \"%s\"...' % p,\n-            sys.stdout.flush()\n+            out.start('Sending patch \"%s\"' % p)\n             __send_message(smtpserver, from_addr, to_addr_list, msg_string,\n                            sleep, smtpuser, smtppassword)\n-            print 'done'\n+            out.done()\ndiff --git a/stgit/commands/patches.py b/stgit/commands/patches.py\nindex dcfbd98..a8fb008 100644\n--- a/stgit/commands/patches.py\n+++ b/stgit/commands/patches.py\n@@ -75,7 +75,7 @@ def func(parser, options, args):\n                                   git.diff(args, patch.get_bottom(),\n                                            patch.get_top()))\n             else:\n-                print patch.get_name()\n+                out.stdout(patch.get_name())\n \n     if options.diff:\n         pager(diff_output)\ndiff --git a/stgit/commands/pick.py b/stgit/commands/pick.py\nindex ea0756b..a47b201 100644\n--- a/stgit/commands/pick.py\n+++ b/stgit/commands/pick.py\n@@ -91,33 +91,30 @@ def func(parser, options, args):\n         top = parent\n \n     if options.fold:\n-        print 'Folding commit %s...' % commit_id,\n-        sys.stdout.flush()\n+        out.start('Folding commit %s' % commit_id)\n \n         # try a direct git-apply first\n         if not git.apply_diff(bottom, top):\n             git.merge(bottom, git.get_head(), top, recursive = True)\n \n-        print 'done'\n+        out.done()\n     elif options.update:\n         rev1 = git_id('//bottom')\n         rev2 = git_id('//top')\n         files = git.barefiles(rev1, rev2).split('\\n')\n \n-        print 'Updating with commit %s...' % commit_id,\n-        sys.stdout.flush()\n+        out.start('Updating with commit %s' % commit_id)\n \n         if not git.apply_diff(bottom, top, files = files):\n             raise CmdException, 'Patch updating failed'\n \n-        print 'done'\n+        out.done()\n     else:\n         message = commit.get_log()\n         author_name, author_email, author_date = \\\n                      name_email_date(commit.get_author())\n \n-        print 'Importing commit %s...' % commit_id,\n-        sys.stdout.flush()\n+        out.start('Importing commit %s' % commit_id)\n \n         newpatch = crt_series.new_patch(patchname, message = message, can_edit = False,\n                                         unapplied = True, bottom = bottom, top = top,\n@@ -137,23 +134,23 @@ def func(parser, options, args):\n                 refseries = crt_series\n             patch = refseries.get_patch(refpatchname)\n             if patch.get_log():\n-                print\"log was %s\" % newpatch.get_log()\n-                print \"setting log to %s\\n\" %  patch.get_log()\n+                out.info(\"Log was %s\" % newpatch.get_log())\n+                out.info(\"Setting log to %s\\n\" %  patch.get_log())\n                 newpatch.set_log(patch.get_log())\n-                print\"log is now %s\" % newpatch.get_log()\n+                out.info(\"Log is now %s\" % newpatch.get_log())\n             else:\n-                print \"no log for %s\\n\" % patchname\n- \n+                out.info(\"No log for %s\\n\" % patchname)\n+\n         if not options.unapplied:\n             modified = crt_series.push_patch(patchname)\n         else:\n             modified = False\n \n         if crt_series.empty_patch(patchname):\n-            print 'done (empty patch)'\n+            out.done('empty patch')\n         elif modified:\n-            print 'done (modified)'\n+            out.done('modified')\n         else:\n-            print 'done'\n-        \n+            out.done()\n+\n     print_crt_patch()\ndiff --git a/stgit/commands/pull.py b/stgit/commands/pull.py\nindex f551b98..beaa7b5 100644\n--- a/stgit/commands/pull.py\n+++ b/stgit/commands/pull.py\n@@ -90,10 +90,10 @@ def func(parser, options, args):\n \n     # pull the remote changes\n     if policy == 'pull':\n-        print 'Pulling from \"%s\"...' % repository\n+        out.info('Pulling from \"%s\"' % repository)\n         git.pull(repository)\n     elif policy == 'fetch-rebase':\n-        print 'Fetching from \"%s\"...' % repository\n+        out.info('Fetching from \"%s\"' % repository)\n         git.fetch(repository)\n         rebase(git.fetch_head())\n     elif policy == 'rebase':\ndiff --git a/stgit/commands/push.py b/stgit/commands/push.py\nindex a636ad2..17b32f6 100644\n--- a/stgit/commands/push.py\n+++ b/stgit/commands/push.py\n@@ -64,13 +64,12 @@ def func(parser, options, args):\n         if not patch:\n             raise CmdException, 'No patch to undo'\n \n-        print 'Undoing the \"%s\" push...' % patch,\n-        sys.stdout.flush()\n+        out.start('Undoing push of \"%s\"' % patch)\n         resolved_all()\n         if crt_series.undo_push():\n-            print 'done'\n+            out.done()\n         else:\n-            print 'done (patch unchanged)'\n+            out.done('patch unchanged')\n         print_crt_patch()\n \n         return\ndiff --git a/stgit/commands/refresh.py b/stgit/commands/refresh.py\nindex 4cf09ae..77dcbda 100644\n--- a/stgit/commands/refresh.py\n+++ b/stgit/commands/refresh.py\n@@ -98,10 +98,9 @@ def func(parser, options, args):\n         check_head_top_equal()\n \n     if options.undo:\n-        print 'Undoing the \"%s\" refresh...' % patch,\n-        sys.stdout.flush()\n+        out.start('Undoing the refresh of \"%s\"' % patch)\n         crt_series.undo_refresh()\n-        print 'done'\n+        out.done()\n         return\n \n     if options.author:\n@@ -128,8 +127,7 @@ def func(parser, options, args):\n             between = applied[:applied.index(patch):-1]\n             pop_patches(between, keep = True)\n \n-        print 'Refreshing patch \"%s\"...' % patch,\n-        sys.stdout.flush()\n+        out.start('Refreshing patch \"%s\"' % patch)\n \n         if autoresolved == 'yes':\n             resolved_all()\n@@ -145,12 +143,12 @@ def func(parser, options, args):\n                                  backup = True, sign_str = sign_str)\n \n         if crt_series.empty_patch(patch):\n-            print 'done (empty patch)'\n+            out.done('empty patch')\n         else:\n-            print 'done'\n+            out.done()\n \n         if options.patch:\n             between.reverse()\n             push_patches(between)\n     else:\n-        print 'Patch \"%s\" is already up to date' % patch\n+        out.info('Patch \"%s\" is already up to date' % patch)\ndiff --git a/stgit/commands/rename.py b/stgit/commands/rename.py\nindex ca799c3..d6c53be 100644\n--- a/stgit/commands/rename.py\n+++ b/stgit/commands/rename.py\n@@ -38,7 +38,6 @@ def func(parser, options, args):\n     if len(args) != 2:\n         parser.error('incorrect number of arguments')\n \n-    print 'Renaming patch \"%s\" -> \"%s\"...' % (args[0], args[1]),\n-    sys.stdout.flush()\n+    out.start('Renaming patch \"%s\" to \"%s\"' % (args[0], args[1]))\n     crt_series.rename_patch(args[0], args[1])\n-    print 'done'\n+    out.done()\ndiff --git a/stgit/commands/series.py b/stgit/commands/series.py\nindex 7777201..b699902 100644\n--- a/stgit/commands/series.py\n+++ b/stgit/commands/series.py\n@@ -102,11 +102,11 @@ def __print_patch(patch, hidden, branch_str, prefix, empty_prefix, length,\n         patch_str = patch_str.ljust(length)\n \n     if options.description:\n-        print prefix + patch_str + ' | ' + __get_description(patch)\n+        out.stdout(prefix + patch_str + ' | ' + __get_description(patch))\n     elif options.author:\n-        print prefix + patch_str + ' | ' + __get_author(patch)\n+        out.stdout(prefix + patch_str + ' | ' + __get_author(patch))\n     else:\n-        print prefix + patch_str\n+        out.stdout(prefix + patch_str)\n \n def func(parser, options, args):\n     \"\"\"Show the patch series\n@@ -157,7 +157,7 @@ def func(parser, options, args):\n     patches = applied + unapplied\n \n     if options.count:\n-        print len(patches)\n+        out.stdout(len(patches))\n         return\n \n     if not patches:\ndiff --git a/stgit/commands/sync.py b/stgit/commands/sync.py\nindex db52bcd..8359061 100644\n--- a/stgit/commands/sync.py\n+++ b/stgit/commands/sync.py\n@@ -74,13 +74,10 @@ def func(parser, options, args):\n                   '--undo cannot be specified with --branch or --series'\n         __check_all()\n \n-        print 'Undoing the \"%s\" sync...' % crt_series.get_current(),\n-        sys.stdout.flush()\n-\n+        out.start('Undoing the sync of \"%s\"' % crt_series.get_current())\n         crt_series.undo_refresh()\n         git.reset()\n-\n-        print 'done'\n+        out.done()\n         return\n \n     if options.branch:\n@@ -147,8 +144,7 @@ def func(parser, options, args):\n             del popped[:idx]\n \n         # the actual sync\n-        print 'Synchronising \"%s\"...' % p,\n-        sys.stdout.flush()\n+        out.start('Synchronising \"%s\"' % p)\n \n         patch = crt_series.get_patch(p)\n         bottom = patch.get_bottom()\n@@ -167,9 +163,9 @@ def func(parser, options, args):\n             # backup information was already reset above\n             crt_series.refresh_patch(cache_update = False, backup = False,\n                                      log = 'sync')\n-            print 'done (updated)'\n+            out.done('updated')\n         else:\n-            print 'done'\n+            out.done()\n \n     # push the remaining patches\n     if popped:\ndiff --git a/stgit/commands/top.py b/stgit/commands/top.py\nindex f36a0b0..7cc92ca 100644\n--- a/stgit/commands/top.py\n+++ b/stgit/commands/top.py\n@@ -41,6 +41,6 @@ def func(parser, options, args):\n \n     name = crt_series.get_current()\n     if name:\n-        print name\n+        out.stdout(name)\n     else:\n         raise CmdException, 'No patches applied'\ndiff --git a/stgit/commands/unapplied.py b/stgit/commands/unapplied.py\nindex cbac052..0d330a1 100644\n--- a/stgit/commands/unapplied.py\n+++ b/stgit/commands/unapplied.py\n@@ -46,7 +46,7 @@ def func(parser, options, args):\n     unapplied = crt_series.get_unapplied()\n \n     if options.count:\n-        print len(unapplied)\n+        out.stdout(len(unapplied))\n     else:\n         for p in unapplied:\n-            print p\n+            out.stdout(p)\ndiff --git a/stgit/commands/uncommit.py b/stgit/commands/uncommit.py\nindex d5f64da..f611d29 100644\n--- a/stgit/commands/uncommit.py\n+++ b/stgit/commands/uncommit.py\n@@ -99,13 +99,13 @@ def func(parser, options, args):\n     commits = []\n     next_commit = crt_series.get_base()\n     if patch_nr:\n-        print 'Uncommitting %d patches...' % patch_nr,\n+        out.start('Uncommitting %d patches' % patch_nr)\n         for i in xrange(patch_nr):\n             commit, commit_id, parent = get_commit(next_commit)\n             commits.append((commit, commit_id, parent))\n             next_commit = parent\n     else:\n-        print 'Uncommitting to %s...' % to_commit\n+        out.start('Uncommitting to %s' % to_commit)\n         while True:\n             commit, commit_id, parent = get_commit(next_commit)\n             commits.append((commit, commit_id, parent))\n@@ -114,7 +114,6 @@ def func(parser, options, args):\n             next_commit = parent\n         patch_nr = len(commits)\n \n-    sys.stdout.flush()\n     for (commit, commit_id, parent), patchname in \\\n         zip(commits, patchnames or [None for i in xrange(len(commits))]):\n         author_name, author_email, author_date = \\\n@@ -127,5 +126,4 @@ def func(parser, options, args):\n                              author_email = author_email,\n                              author_date = author_date)\n \n-\n-    print 'done'\n+    out.done()\ndiff --git a/stgit/commands/unhide.py b/stgit/commands/unhide.py\nindex f2db05f..74e4743 100644\n--- a/stgit/commands/unhide.py\n+++ b/stgit/commands/unhide.py\n@@ -46,4 +46,4 @@ def func(parser, options, args):\n \n     for patch in patches:\n         crt_series.unhide_patch(patch)\n-        print 'Patch \"%s\" unhidden' % patch\n+        out.info('Patch \"%s\" unhidden' % patch)\ndiff --git a/stgit/git.py b/stgit/git.py\nindex 86630ce..845c712 100644\n--- a/stgit/git.py\n+++ b/stgit/git.py\n@@ -220,9 +220,8 @@ def __tree_status(files = None, tree_id = 'HEAD', unknown = False,\n                   noexclude = True, verbose = False):\n     \"\"\"Returns a list of pairs - [status, filename]\n     \"\"\"\n-    if verbose and sys.stdout.isatty():\n-        print 'Checking for changes in the working directory...',\n-        sys.stdout.flush()\n+    if verbose:\n+        out.start('Checking for changes in the working directory')\n \n     refresh_index()\n \n@@ -260,8 +259,8 @@ def __tree_status(files = None, tree_id = 'HEAD', unknown = False,\n         if fs[1] not in conflicts:\n             cache_files.append(fs)\n \n-    if verbose and sys.stdout.isatty():\n-        print 'done'\n+    if verbose:\n+        out.done()\n \n     return cache_files\n \n@@ -449,8 +448,7 @@ def __copy_single(source, target, target2=''):\n         for f in [f.strip() for f in realfiles]:\n             m = prefix_regexp.match(f)\n             if not m:\n-                print '\"%s\" does not match \"%s\"' % (f, re_string)\n-                assert(m)\n+                raise Exception, '\"%s\" does not match \"%s\"' % (f, re_string)\n             newname = target+target2+'/'+m.group(1)\n             if not os.path.exists(os.path.dirname(newname)):\n                 os.makedirs(os.path.dirname(newname))\n@@ -767,9 +765,9 @@ def status(files = None, modified = False, new = False, deleted = False,\n         if files and not fs[1] in files:\n             continue\n         if all:\n-            print '%s %s' % (fs[0], fs[1])\n+            out.stdout('%s %s' % (fs[0], fs[1]))\n         else:\n-            print '%s' % fs[1]\n+            out.stdout('%s' % fs[1])\n \n def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None,\n          binary = False):\n@@ -958,7 +956,7 @@ def apply_patch(filename = None, diff = None, base = None,\n             f = file('.stgit-failed.patch', 'w+')\n             f.write(diff)\n             f.close()\n-            print >> sys.stderr, 'Diff written to the .stgit-failed.patch file'\n+            out.warn('Diff written to the .stgit-failed.patch file')\n \n         raise\n \ndiff --git a/stgit/gitmergeonefile.py b/stgit/gitmergeonefile.py\nindex 6f4c1d7..303e5f7 100644\n--- a/stgit/gitmergeonefile.py\n+++ b/stgit/gitmergeonefile.py\n@@ -21,7 +21,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n import sys, os\n from stgit import basedir\n from stgit.config import config, file_extensions, ConfigOption\n-from stgit.utils import append_string\n+from stgit.utils import append_string, out\n \n \n class GitMergeException(Exception):\n@@ -133,8 +133,8 @@ def interactive_merge(filename):\n \n     mtime = os.path.getmtime(filename)\n \n-    print 'Trying the interractive %s merge' % \\\n-          (three_way and 'three-way' or 'two-way')\n+    out.info('Trying the interactive %s merge'\n+             % (three_way and 'three-way' or 'two-way'))\n \n     err = os.system(imerger % files_dict)\n     if err != 0:\n@@ -165,16 +165,15 @@ def merge(orig_hash, file1_hash, file2_hash,\n             if file1_hash == file2_hash:\n                 if os.system('git-update-index --cacheinfo %s %s %s'\n                              % (file1_mode, file1_hash, path)) != 0:\n-                    print >> sys.stderr, 'Error: git-update-index failed'\n+                    out.error('git-update-index failed')\n                     __conflict(path)\n                     return 1\n                 if os.system('git-checkout-index -u -f -- %s' % path):\n-                    print >> sys.stderr, 'Error: git-checkout-index failed'\n+                    out.error('git-checkout-index failed')\n                     __conflict(path)\n                     return 1\n                 if file1_mode != file2_mode:\n-                    print >> sys.stderr, \\\n-                          'Error: File added in both, permissions conflict'\n+                    out.error('File added in both, permissions conflict')\n                     __conflict(path)\n                     return 1\n             # 3-way merge\n@@ -189,9 +188,8 @@ def merge(orig_hash, file1_hash, file2_hash,\n                     __remove_files(orig_hash, file1_hash, file2_hash)\n                     return 0\n                 else:\n-                    print >> sys.stderr, \\\n-                          'Error: three-way merge tool failed for file \"%s\"' \\\n-                          % path\n+                    out.error('Three-way merge tool failed for file \"%s\"'\n+                              % path)\n                     # reset the cache to the first branch\n                     os.system('git-update-index --cacheinfo %s %s %s'\n                               % (file1_mode, file1_hash, path))\n@@ -201,7 +199,7 @@ def merge(orig_hash, file1_hash, file2_hash,\n                             interactive_merge(path)\n                         except GitMergeException, ex:\n                             # interactive merge failed\n-                            print >> sys.stderr, str(ex)\n+                            out.error(ex)\n                             if str(keeporig) != 'yes':\n                                 __remove_files(orig_hash, file1_hash,\n                                                file2_hash)\n@@ -260,23 +258,21 @@ def merge(orig_hash, file1_hash, file2_hash,\n             if file1_hash == file2_hash:\n                 if os.system('git-update-index --add --cacheinfo %s %s %s'\n                              % (file1_mode, file1_hash, path)) != 0:\n-                    print >> sys.stderr, 'Error: git-update-index failed'\n+                    out.error('git-update-index failed')\n                     __conflict(path)\n                     return 1\n                 if os.system('git-checkout-index -u -f -- %s' % path):\n-                    print >> sys.stderr, 'Error: git-checkout-index failed'\n+                    out.error('git-checkout-index failed')\n                     __conflict(path)\n                     return 1\n                 if file1_mode != file2_mode:\n-                    print >> sys.stderr, \\\n-                          'Error: File \"s\" added in both, ' \\\n-                          'permissions conflict' % path\n+                    out.error('File \"s\" added in both, permissions conflict'\n+                              % path)\n                     __conflict(path)\n                     return 1\n             # files added in both but different\n             else:\n-                print >> sys.stderr, \\\n-                      'Error: File \"%s\" added in branches but different' % path\n+                out.error('File \"%s\" added in branches but different' % path)\n                 # reset the cache to the first branch\n                 os.system('git-update-index --cacheinfo %s %s %s'\n                           % (file1_mode, file1_hash, path))\n@@ -286,7 +282,7 @@ def merge(orig_hash, file1_hash, file2_hash,\n                         interactive_merge(path)\n                     except GitMergeException, ex:\n                         # interactive merge failed\n-                        print >> sys.stderr, str(ex)\n+                        out.error(ex)\n                         if str(keeporig) != 'yes':\n                             __remove_files(orig_hash, file1_hash,\n                                            file2_hash)\n@@ -312,17 +308,16 @@ def merge(orig_hash, file1_hash, file2_hash,\n                 obj = file2_hash\n             if os.system('git-update-index --add --cacheinfo %s %s %s'\n                          % (mode, obj, path)) != 0:\n-                print >> sys.stderr, 'Error: git-update-index failed'\n+                out.error('git-update-index failed')\n                 __conflict(path)\n                 return 1\n             __remove_files(orig_hash, file1_hash, file2_hash)\n             return os.system('git-checkout-index -u -f -- %s' % path)\n \n     # Unhandled case\n-    print >> sys.stderr, 'Error: Unhandled merge conflict: ' \\\n-          '\"%s\" \"%s\" \"%s\" \"%s\" \"%s\" \"%s\" \"%s\"' \\\n-          % (orig_hash, file1_hash, file2_hash,\n-             path,\n-             orig_mode, file1_mode, file2_mode)\n+    out.error('Unhandled merge conflict: \"%s\" \"%s\" \"%s\" \"%s\" \"%s\" \"%s\" \"%s\"'\n+              % (orig_hash, file1_hash, file2_hash,\n+                 path,\n+                 orig_mode, file1_mode, file2_mode))\n     __conflict(path)\n     return 1\ndiff --git a/stgit/main.py b/stgit/main.py\nindex 1a1f534..bcadbad 100644\n--- a/stgit/main.py\n+++ b/stgit/main.py\n@@ -36,14 +36,12 @@ class Commands(dict):\n         candidates = [cmd for cmd in self.keys() if cmd.startswith(key)]\n \n         if not candidates:\n-            print >> sys.stderr, 'Unknown command: %s' % key\n-            print >> sys.stderr, '  Try \"%s help\" for a list of ' \\\n-                  'supported commands' % prog\n+            out.error('Unknown command: %s' % key,\n+                      'Try \"%s help\" for a list of supported commands' % prog)\n             sys.exit(1)\n         elif len(candidates) > 1:\n-            print >> sys.stderr, 'Ambiguous command: %s' % key\n-            print >> sys.stderr, '  Candidates are: %s' \\\n-                  % ', '.join(candidates)\n+            out.error('Ambiguous command: %s' % key,\n+                      'Candidates are: %s' % ', '.join(candidates))\n             sys.exit(1)\n \n         return candidates[0]\n@@ -220,8 +218,7 @@ def main():\n         if len(sys.argv) == 3 and not sys.argv[2] in ['-h', '--help']:\n             cmd = commands.canonical_cmd(sys.argv[2])\n             if not cmd in commands:\n-                print >> sys.stderr, '%s help: \"%s\" command unknown' \\\n-                      % (prog, cmd)\n+                out.error('%s help: \"%s\" command unknown' % (prog, cmd))\n                 sys.exit(1)\n \n             sys.argv[0] += ' %s' % cmd\n@@ -267,7 +264,7 @@ def main():\n     except KeyError:\n         debug_level = 0\n     except ValueError:\n-        print >> sys.stderr, 'Invalid STGIT_DEBUG_LEVEL environment variable'\n+        out.error('Invalid STGIT_DEBUG_LEVEL environment variable')\n         sys.exit(1)\n \n     try:\ndiff --git a/stgit/stack.py b/stgit/stack.py\nindex 223f3ee..63076af 100644\n--- a/stgit/stack.py\n+++ b/stgit/stack.py\n@@ -307,6 +307,7 @@ def update_to_current_format_version(branch, git_dir):\n             # The branch doesn't seem to be initialized at all.\n             return None\n     def set_format_version(v):\n+        out.info('Upgraded branch %s to format version %d' % (branch, v))\n         config.set(format_version_key(branch), '%d' % v)\n     def mkdir(d):\n         if not os.path.isdir(d):\n@@ -491,10 +492,11 @@ class Series(StgitObject):\n         if value:\n             return value\n         elif 'origin' in git.remotes_list():\n-            print 'Notice: no parent remote declared for stack \"%s\", ' \\\n-                  'defaulting to \"origin\". Consider setting \"branch.%s.remote\" ' \\\n-                  'and \"branch.%s.merge\" with \"git repo-config\".' \\\n-                  % (self.__name, self.__name, self.__name)\n+            out.note(('No parent remote declared for stack \"%s\",'\n+                      ' defaulting to \"origin\".' % self.__name),\n+                     ('Consider setting \"branch.%s.remote\" and'\n+                      ' \"branch.%s.merge\" with \"git repo-config\".'\n+                      % (self.__name, self.__name)))\n             return 'origin'\n         else:\n             raise StackException, 'Cannot find a parent remote for \"%s\"' % self.__name\n@@ -507,10 +509,10 @@ class Series(StgitObject):\n         if value:\n             return value\n         elif git.rev_parse('heads/origin'):\n-            print 'Notice: no parent branch declared for stack \"%s\", ' \\\n-                  'defaulting to \"heads/origin\". Consider setting ' \\\n-                  '\"branch.%s.stgit.parentbranch\" with \"git repo-config\".' \\\n-                  % (self.__name, self.__name)\n+            out.note(('No parent branch declared for stack \"%s\",'\n+                      ' defaulting to \"heads/origin\".' % self.__name),\n+                     ('Consider setting \"branch.%s.stgit.parentbranch\"'\n+                      ' with \"git repo-config\".' % self.__name))\n             return 'heads/origin'\n         else:\n             raise StackException, 'Cannot find a parent branch for \"%s\"' % self.__name\n@@ -646,10 +648,10 @@ class Series(StgitObject):\n                                             author_email = patch.get_authemail(),\n                                             author_date = patch.get_authdate())\n             if patch.get_log():\n-                print \"setting log to %s\" %  patch.get_log()\n+                out.info('Setting log to %s' %  patch.get_log())\n                 newpatch.set_log(patch.get_log())\n             else:\n-                print \"no log for %s\" % p\n+                out.info('No log for %s' % p)\n \n         # fast forward the cloned series to self's top\n         new_series.forward_patches(applied)\n@@ -697,17 +699,18 @@ class Series(StgitObject):\n             if not os.listdir(self.__patch_dir):\n                 os.rmdir(self.__patch_dir)\n             else:\n-                print 'Patch directory %s is not empty.' % self.__patch_dir\n+                out.warn('Patch directory %s is not empty' % self.__patch_dir)\n \n             try:\n                 os.removedirs(self._dir())\n             except OSError:\n-                raise StackException, 'Series directory %s is not empty.' % self._dir()\n+                raise StackException('Series directory %s is not empty'\n+                                     % self._dir())\n \n             try:\n                 os.removedirs(self.__refs_dir)\n             except OSError:\n-                print 'Refs directory %s is not empty.' % self.__refs_dir\n+                out.warn('Refs directory %s is not empty' % self.__refs_dir)\n \n         # Cleanup parent informations\n         # FIXME: should one day make use of git-config --section-remove,\n@@ -1046,10 +1049,9 @@ class Series(StgitObject):\n                 try:\n                     git.merge(bottom, head, top, recursive = True)\n                 except git.GitException, ex:\n-                    print >> sys.stderr, \\\n-                          'The merge failed during \"push\". ' \\\n-                          'Use \"refresh\" after fixing the conflicts or ' \\\n-                          'revert the operation with \"push --undo\".'\n+                    out.error('The merge failed during \"push\".',\n+                              'Use \"refresh\" after fixing the conflicts or'\n+                              ' revert the operation with \"push --undo\".')\n \n         append_string(self.__applied_file, name)\n \ndiff --git a/stgit/utils.py b/stgit/utils.py\nindex fbfe748..8df9103 100644\n--- a/stgit/utils.py\n+++ b/stgit/utils.py\n@@ -21,6 +21,86 @@ along with this program; if not, write to the Free Software\n Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n \"\"\"\n \n+class MessagePrinter(object):\n+    def __init__(self):\n+        class Output(object):\n+            def __init__(self, write, flush):\n+                self.write = write\n+                self.flush = flush\n+                self.at_start_of_line = True\n+                self.level = 0\n+            def new_line(self):\n+                \"\"\"Ensure that we're at the beginning of a line.\"\"\"\n+                if not self.at_start_of_line:\n+                    self.write('\\n')\n+                    self.at_start_of_line = True\n+            def single_line(self, msg, print_newline = True,\n+                            need_newline = True):\n+                \"\"\"Write a single line. Newline before and after are\n+                separately configurable.\"\"\"\n+                if need_newline:\n+                    self.new_line()\n+                if self.at_start_of_line:\n+                    self.write('  '*self.level)\n+                self.write(msg)\n+                if print_newline:\n+                    self.write('\\n')\n+                    self.at_start_of_line = True\n+                else:\n+                    self.flush()\n+                    self.at_start_of_line = False\n+            def tagged_lines(self, tag, lines):\n+                tag += ': '\n+                for line in lines:\n+                    self.single_line(tag + line)\n+                    tag = ' '*len(tag)\n+            def write_line(self, line):\n+                \"\"\"Write one line of text on a lines of its own, not\n+                indented.\"\"\"\n+                self.new_line()\n+                self.write('%s\\n' % line)\n+                self.at_start_of_line = True\n+            def write_raw(self, string):\n+                \"\"\"Write an arbitrary string, possibly containing\n+                newlines.\"\"\"\n+                self.new_line()\n+                self.write(string)\n+                self.at_start_of_line = string.endswith('\\n')\n+        self.__stdout = Output(sys.stdout.write, sys.stdout.flush)\n+        if sys.stdout.isatty():\n+            self.__out = self.__stdout\n+        else:\n+            self.__out = Output(lambda msg: None, lambda: None)\n+    def stdout(self, line):\n+        \"\"\"Write a line to stdout.\"\"\"\n+        self.__stdout.write_line(line)\n+    def stdout_raw(self, string):\n+        \"\"\"Write a string possibly containing newlines to stdout.\"\"\"\n+        self.__stdout.write_raw(string)\n+    def info(self, *msgs):\n+        for msg in msgs:\n+            self.__out.single_line(msg)\n+    def note(self, *msgs):\n+        self.__out.tagged_lines('Notice', msgs)\n+    def warn(self, *msgs):\n+        self.__out.tagged_lines('Warning', msgs)\n+    def error(self, *msgs):\n+        self.__out.tagged_lines('Error', msgs)\n+    def start(self, msg):\n+        \"\"\"Start a long-running operation.\"\"\"\n+        self.__out.single_line('%s ... ' % msg, print_newline = False)\n+        self.__out.level += 1\n+    def done(self, extramsg = None):\n+        \"\"\"Finish long-running operation.\"\"\"\n+        self.__out.level -= 1\n+        if extramsg:\n+            msg = 'done (%s)'\n+        else:\n+            msg = 'done'\n+        self.__out.single_line(msg, need_newline = False)\n+\n+out = MessagePrinter()\n+\n def mkdir_file(filename, mode):\n     \"\"\"Opens filename with the given mode, creating the directory it's\n     in if it doesn't already exist.\"\"\"\n@@ -166,12 +246,11 @@ def call_editor(filename):\n         editor = 'vi'\n     editor += ' %s' % filename\n \n-    print 'Invoking the editor: \"%s\"...' % editor,\n-    sys.stdout.flush()\n+    out.start('Invoking the editor: \"%s\"' % editor)\n     err = os.system(editor)\n     if err:\n         raise EditorException, 'editor failed, exit code: %d' % err\n-    print 'done'\n+    out.done()\n \n def patch_name_from_msg(msg):\n     \"\"\"Return a string to be used as a patch name. This is generated\n"}]}