Added --allow-empty to "be commit"
authorW. Trevor King <wking@drexel.edu>
Sun, 19 Jul 2009 19:24:51 +0000 (15:24 -0400)
committerW. Trevor King <wking@drexel.edu>
Sun, 19 Jul 2009 19:24:51 +0000 (15:24 -0400)
Previously many backends would silently add an empty commit.  Not very
useful.  When the new --allow-empty flag and related allow_empty
options are false, every versioning backend is guaranteed to raise the
EmptyCommit exception in the case of an attempted empty commit.

becommands/commit.py
libbe/arch.py
libbe/bzr.py
libbe/cmdutil.py
libbe/darcs.py
libbe/git.py
libbe/hg.py
libbe/rcs.py
test_usage.sh

index bda51c490b37cac4c4f641bc11aea1bf9b3ff03f..4f3bdbda07dd3c9939a1154817e05d69b4f5687a 100644 (file)
@@ -14,7 +14,7 @@
 # with this program; if not, write to the Free Software Foundation, Inc.,
 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 """Commit the currently pending changes to the repository"""
-from libbe import cmdutil, bugdir, editor
+from libbe import cmdutil, bugdir, editor, rcs
 import sys
 __desc__ = __doc__
 
@@ -49,13 +49,22 @@ def execute(args, manipulate_encodings=True):
         body = editor.editor_string("Please enter your commit message above")
     else:
         body = bd.rcs.get_file_contents(options.body, allow_no_rcs=True)
-    revision = bd.rcs.commit(summary, body=body)
-    print "Committed %s" % revision
+    try:
+        revision = bd.rcs.commit(summary, body=body,
+                                 allow_empty=options.allow_empty)
+    except rcs.EmptyCommit, e:
+        print e
+        return 1
+    else:
+        print "Committed %s" % revision
 
 def get_parser():
     parser = cmdutil.CmdOptionParser("be commit COMMENT")
     parser.add_option("-b", "--body", metavar="FILE", dest="body",
                       help='Provide a detailed body for the commit message.  In the special case that FILE == "EDITOR", spawn an editor to enter the body text (in which case you cannot use stdin for the summary)', default=None)
+    parser.add_option("-a", "--allow-empty", dest="allow_empty",
+                      help="Allow empty commits",
+                      default=False, action="store_true")
     return parser
 
 longhelp="""
index 30983e7175eee3cc08c7b9cfbc982215757265bb..2f45aa924fa497ff23ef0e84051be6bc89eb536f 100644 (file)
@@ -260,16 +260,17 @@ class Arch(RCS):
         else:
             status,output,error = \
                 self._u_invoke_client("get", revision,directory)
-    def _rcs_commit(self, commitfile):
+    def _rcs_commit(self, commitfile, allow_empty=False):
+        if allow_empty == False:
+            # arch applies empty commits without complaining, so check first
+            status,output,error = self._u_invoke_client("changes",expect=(0,1))
+            if status == 0:
+                raise rcs.EmptyCommit()
         summary,body = self._u_parse_commitfile(commitfile)
-        #status,output,error = self._invoke_client("make-log")
-        if body == None:
-            status,output,error \
-                = self._u_invoke_client("commit","--summary",summary)
-        else:
-            status,output,error \
-                = self._u_invoke_client("commit","--summary",summary,
-                                        "--log-message",body)
+        args = ["commit", "--summary", summary]
+        if body != None:
+            args.extend(["--log-message",body])
+        status,output,error = self._u_invoke_client(*args)
         revision = None
         revline = re.compile("[*] committed (.*)")
         match = revline.search(output)
index fcbd6ac5aa201ca6aa8a6ac656caf288c531e524..b33292c1ef1e1bbf676e21622af637c937af197e 100644 (file)
@@ -71,9 +71,21 @@ class Bzr(RCS):
         else:
             self._u_invoke_client("branch", "--revision", revision,
                                   ".", directory)
-    def _rcs_commit(self, commitfile):
-        status,output,error = self._u_invoke_client("commit", "--unchanged",
-                                                    "--file", commitfile)
+    def _rcs_commit(self, commitfile, allow_empty=False):
+        args = ["commit", "--file", commitfile]
+        if allow_empty == True:
+            args.append("--unchanged")
+            status,output,error = self._u_invoke_client(*args)
+        else:
+            kwargs = {"expect":(0,3)}
+            status,output,error = self._u_invoke_client(*args, **kwargs)
+            if status != 0:
+                strings = ["ERROR: no changes to commit.", # bzr 1.3.1
+                           "ERROR: No changes to commit."] # bzr 1.15.1
+                if self._u_any_in_string(strings, error) == True:
+                    raise rcs.EmptyCommit()
+                else:
+                    raise rcs.CommandError(args, status, error)
         revision = None
         revline = re.compile("Committed revision (.*)[.]")
         match = revline.search(error)
index bba3e0ead729d7f21820e50b2aaf0ff8aa085e6f..853a75a406f53fa863a9263ad5ff12566f5df41a 100644 (file)
@@ -73,8 +73,10 @@ def get_command(command_name):
 def execute(cmd, args):
     enc = encoding.get_encoding()
     cmd = get_command(cmd)
-    cmd.execute([a.decode(enc) for a in args])
-    return 0
+    ret = cmd.execute([a.decode(enc) for a in args])
+    if ret == None:
+        ret = 0
+    return ret
 
 def help(cmd=None, parser=None):
     if cmd != None:
index 1beb45d16be6fe83feec1c8ce66ee1c108055a8b..e7132c017cafe2834d11ca3925e8a0b6c4e34eb3 100644 (file)
@@ -131,24 +131,25 @@ class Darcs(RCS):
             RCS._rcs_duplicate_repo(self, directory, revision)
         else:
             self._u_invoke_client("put", "--to-patch", revision, directory)
-    def _rcs_commit(self, commitfile):
+    def _rcs_commit(self, commitfile, allow_empty=False):
         id = self.get_user_id()
         if '@' not in id:
             id = "%s <%s@invalid.com>" % (id, id)
-        # Darcs doesn't like commitfiles without trailing endlines.
-        f = codecs.open(commitfile, 'r', self.encoding)
-        contents = f.read()
-        f.close()
-        if contents[-1] != '\n':
-            f = codecs.open(commitfile, 'a', self.encoding)
-            f.write('\n')
-            f.close()
-        status,output,error = self._u_invoke_client('record', '--all',
-                                                    '--author', id,
-                                                    '--logfile', commitfile)
+        args = ['record', '--all', '--author', id, '--logfile', commitfile]
+        status,output,error = self._u_invoke_client(*args)
+        empty_strings = ["No changes!"]
         revision = None
-
-        revline = re.compile("Finished recording patch '(.*)'")
+        if self._u_any_in_string(empty_strings, output) == True:
+            if allow_empty == False:
+                raise rcs.EmptyCommit()
+            else: # we need a extra call to get the current revision
+                args = ["changes", "--last=1", "--xml"]
+                status,output,error = self._u_invoke_client(*args)
+                revline = re.compile("[ \t]*<name>(.*)</name>")
+                # note that darcs does _not_ make an empty revision.
+                # this returns the last non-empty revision id...
+        else:
+            revline = re.compile("Finished recording patch '(.*)'")
         match = revline.search(output)
         assert match != None, output+error
         assert len(match.groups()) == 1
index 4a91d4489898efc48cb0c2c129b4a07e5fe6e9b4..2f9ffa9500f97ba7bb0e8d4d50980b82b07bcc64 100644 (file)
@@ -93,9 +93,18 @@ class Git(RCS):
             #self._u_invoke_client("archive", revision, directory) # makes tarball
             self._u_invoke_client("clone", "--no-checkout",".",directory)
             self._u_invoke_client("checkout", revision, directory=directory)
-    def _rcs_commit(self, commitfile):
-        status,output,error = self._u_invoke_client('commit', '-a',
-                                                    '-F', commitfile)
+    def _rcs_commit(self, commitfile, allow_empty=False):
+        args = ['commit', '--all', '--file', commitfile]
+        if allow_empty == True:
+            args.append("--allow-empty")
+            status,output,error = self._u_invoke_client(*args)
+        else:
+            kwargs = {"expect":(0,1)}
+            status,output,error = self._u_invoke_client(*args, **kwargs)
+            strings = ["nothing to commit",
+                       "nothing added to commit"]
+            if self._u_any_in_string(strings, output) == True:
+                raise rcs.EmptyCommit()
         revision = None
         revline = re.compile("(.*) (.*)[:\]] (.*)")
         match = revline.search(output)
index c3019481d35dae1cdc13625d8cf1061c6f90d8a7..a20eeb5ca73f49e3daf7c4759c69cced8de37786 100644 (file)
@@ -58,7 +58,7 @@ class Hg(RCS):
     def _rcs_add(self, path):
         self._u_invoke_client("add", path)
     def _rcs_remove(self, path):
-        self._u_invoke_client("rm", path)
+        self._u_invoke_client("rm", "--force", path)
     def _rcs_update(self, path):
         pass
     def _rcs_get_file_contents(self, path, revision=None, binary=False):
@@ -73,8 +73,13 @@ class Hg(RCS):
             return RCS._rcs_duplicate_repo(self, directory, revision)
         else:
             self._u_invoke_client("archive", "--rev", revision, directory)
-    def _rcs_commit(self, commitfile):
-        self._u_invoke_client('commit', '--logfile', commitfile)
+    def _rcs_commit(self, commitfile, allow_empty=False):
+        args = ['commit', '--logfile', commitfile]
+        status,output,error = self._u_invoke_client(*args)
+        if allow_empty == False:
+            strings = ["nothing changed"]
+            if self._u_any_in_string(strings, output) == True:
+                raise rcs.EmptyCommit()
         status,output,error = self._u_invoke_client('identify')
         revision = None
         revline = re.compile("(.*) tip")
index 7138d0148cef915cdb985c62fbcdb784b68d6d9e..3bf8c9d78ca0f3074a896b3cfb95f47a41910b7f 100644 (file)
@@ -61,10 +61,13 @@ def installed_rcs():
 
 
 class CommandError(Exception):
-    def __init__(self, err_str, status):
-        Exception.__init__(self, "Command failed (%d): %s" % (status, err_str))
-        self.err_str = err_str
+    def __init__(self, command, status, err_str):
+        strerror = ["Command failed (%d):\n  %s\n" % (status, err_str),
+                    "while executing\n  %s" % command]
+        Exception.__init__(self, "\n".join(strerror))
+        self.command = command
         self.status = status
+        self.err_str = err_str
 
 class SettingIDnotSupported(NotImplementedError):
     pass
@@ -86,6 +89,10 @@ class NoSuchFile(Exception):
         path = os.path.abspath(os.path.join(root, pathname))
         Exception.__init__(self, "No such file: %s" % path)
 
+class EmptyCommit(Exception):
+    def __init__(self):
+        Exception.__init__(self, "No changes to commit")
+
 
 def new():
     return RCS()
@@ -197,11 +204,14 @@ class RCS(object):
         dir specifies a directory to create the duplicate in.
         """
         shutil.copytree(self.rootdir, directory, True)
-    def _rcs_commit(self, commitfile):
+    def _rcs_commit(self, commitfile, allow_empty=False):
         """
         Commit the current working directory, using the contents of
         commitfile as the comment.  Return the name of the old
         revision (or None if commits are not supported).
+        
+        If allow_empty == False, raise EmptyCommit if there are no
+        changes to commit.
         """
         return None
     def installed(self):
@@ -364,22 +374,25 @@ class RCS(object):
             shutil.rmtree(self._duplicateBasedir)
             self._duplicateBasedir = None
             self._duplicateDirname = None
-    def commit(self, summary, body=None):
+    def commit(self, summary, body=None, allow_empty=False):
         """
         Commit the current working directory, with a commit message
         string summary and body.  Return the name of the old revision
         (or None if versioning is not supported).
+        
+        If allow_empty == False (the default), raise EmptyCommit if
+        there are no changes to commit.
         """
-        summary = summary.strip()
+        summary = summary.strip()+'\n'
         if body is not None:
-            summary += '\n\n' + body.strip() + '\n'
+            summary += '\n' + body.strip() + '\n'
         descriptor, filename = tempfile.mkstemp()
         revision = None
         try:
             temp_file = os.fdopen(descriptor, 'wb')
             temp_file.write(summary)
             temp_file.flush()
-            revision = self._rcs_commit(filename)
+            revision = self._rcs_commit(filename, allow_empty=allow_empty)
             temp_file.close()
         finally:
             os.remove(filename)
@@ -388,7 +401,20 @@ class RCS(object):
         pass
     def postcommit(self, directory):
         pass
+    def _u_any_in_string(self, list, string):
+        """
+        Return True if any of the strings in list are in string.
+        Otherwise return False.
+        """
+        for list_string in list:
+            if list_string in string:
+                return True
+        return False
     def _u_invoke(self, args, stdin=None, expect=(0,), cwd=None):
+        """
+        expect should be a tuple of allowed exit codes.  cwd should be
+        the directory from which the command will be executed.
+        """
         if cwd == None:
             cwd = self.rootdir
         if self.verboseInvoke == True:
@@ -401,15 +427,13 @@ class RCS(object):
                 q = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, 
                           shell=True, cwd=cwd)
         except OSError, e :
-            strerror = "%s\nwhile executing %s" % (e.args[1], args)
-            raise CommandError(strerror, e.args[0])
+            raise CommandError(args, e.args[0], e)
         output, error = q.communicate(input=stdin)
         status = q.wait()
         if self.verboseInvoke == True:
             print >> sys.stderr, "%d\n%s%s" % (status, output, error)
         if status not in expect:
-            strerror = "%s\nwhile executing %s\n%s" % (args[1], args, error)
-            raise CommandError(strerror, status)
+            raise CommandError(args, status, error)
         return status, output, error
     def _u_invoke_client(self, *args, **kwargs):
         directory = kwargs.get('directory',None)
index 48d572d304091ba7c5dad80e899f9e1f87096fc7..13be2ff75a4501a790d091578071f4146c09707c 100755 (executable)
@@ -18,6 +18,8 @@ set -v # verbose, echo commands to stdout
 exec 6>&2 # save stderr to file descriptor 6
 exec 2>&1 # fd 2 now writes to stdout
 
+ONLY_TEST_COMMIT="true"
+
 if [ $# -gt 1 ]
 then
     echo "usage: test_usage.sh [RCS]"
@@ -130,6 +132,9 @@ BUGC=`echo "$OUT" | sed -n 's/Created bug with ID //p'`
 be comment $BUGC "The ants go marching..."
 be show --xml $BUGC | be comment --xml ${BUG}:2 -
 be remove $BUG # decide that you don't like that bug after all
+be commit "You can even commit using BE"
+be commit --allow-empty "And you can add empty commits if you like"
+be commit "But this will fail" || echo "Failed"
 
 cd /
 rm -rf $TESTDIR