source: mergebot/trunk/mergebot/svn.py @ 47

Last change on this file since 47 was 39, checked in by retracile, 15 years ago

Mergebot: fix comparison bug

File size: 6.5 KB
Line 
1#!/usr/bin/python
2"""
3Encapsulate logical Subversion operations so the various MergeBot actors can
4operate at a higher level of abstraction.
5"""
6
7import os
8import time
9import re
10import subprocess
11
12def shell_quote(string):
13    """Given a string, escape the characters interpretted by the shell."""
14    for char in ["\\", "\"", "$"]:
15        string = string.replace(char, "\\%s" % (char, ))
16    return '"%s"' % (string, )
17
18
19class SvnLib(object):
20    """A library to provide a higher-level set of subversion operations."""
21    def __init__(self):
22        self.svn_version = self.get_svn_version()
23
24    def get_svn_version(self):
25        svn = subprocess.Popen(['svn', '--version', '--quiet'],
26                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
27        version_string, _stderr = svn.communicate()
28        return [int(x) for x in version_string.split('.')]
29       
30    def logcmd(self, cmd, logfile):
31        """Log the cmd string, then execute it, appending its stdout and stderr
32        to logfile."""
33        open(logfile, "a").write("%s: %s\n" % (time.asctime(), cmd))
34        return os.system("(%s) >>%s 2>&1" % (cmd, logfile))
35
36    def get_rev_from_log(self, logentry):
37        """Given a log entry split out of svn log, return its revision number"""
38        return int(logentry.split()[0][1:])
39
40    def does_url_exist_14(self, url):
41        """Given a subversion url return true if it exists, false otherwise."""
42        return not subprocess.call(['svn', 'log', '--limit=1',
43                                    '--non-interactive', url],
44                        stdout=open('/dev/null', 'w'),
45                        stderr=open('/dev/null', 'w'))
46
47    def does_url_exist_15(self, url):
48        """Given a subversion url return true if it exists, false otherwise."""
49        return not subprocess.call(['svn', 'ls', '--depth', 'empty',
50                                    '--non-interactive', url],
51                        stdout=open('/dev/null', 'w'),
52                        stderr=open('/dev/null', 'w'))
53
54    def does_url_exist(self, url):
55        if self.svn_version[:2] == [1,4]:
56            return self.does_url_exist_14(url)
57        return self.does_url_exist_15(url)
58
59    def get_branch_info(self, url, logfile):
60        """Given a subversion url and a logfile, return (start_revision,
61        end_revision) or None if it does not exist."""
62        svncmd = os.popen("svn log --stop-on-copy --non-interactive %s 2>>%s" \
63            % (url, logfile), "r")
64        branchlog = svncmd.read()
65        returnval = svncmd.close()
66        if returnval:
67            # This branch apparently doesn't exist
68            return None
69        logs = branchlog.split("-"*72 + "\n")
70        # If there have been no commits on the branch since it was created,
71        # there will only be one revision listed.... but the log will split
72        # into 3 parts.
73        endrev = self.get_rev_from_log(logs[1])
74        startrev = self.get_rev_from_log(logs[-2])
75        return (startrev, endrev)
76
77    def create_branch(self, from_url, to_url, commit_message, logfile):
78        """Create a branch copying from_url to to_url.  Commit as mergebot, and
79        use the provided commit message."""
80        svncmd = \
81            "svn copy --username=mergebot --password=mergebot -m %s %s %s" \
82            % (shell_quote(commit_message), from_url, to_url)
83        return self.logcmd(svncmd, logfile)
84
85    def delete_branch(self, url, commit_message, logfile):
86        """This will generate a new revision.  Return the revision number, or
87        -1 on failure.
88        Assumes that the url exists.  You should call get_branch_info() to
89        determine that first"""
90        svncmd = "svn rm --no-auth-cache " \
91            "--username=mergebot --password=mergebot " \
92            "-m %s %s 2>>%s" % (shell_quote(commit_message), url, logfile)
93        return self._svn_new_rev_command(svncmd)
94
95    def checkout(self, from_url, workingdir, logfile):
96        """Checkout from the given url into workingdir"""
97        return os.system("svn checkout %s %s >>%s 2>&1" % (from_url, workingdir,
98            logfile))
99
100    def merge(self, from_url, workingdir, revision_range, logfile):
101        """Returns a list (status, filename) tuples"""
102        # There are a couple of different 'Skipped' messages.
103        skipped_regex = re.compile("Skipped.* '(.*)'", re.M)
104        start_rev, end_rev = revision_range
105        pipe = os.popen("cd %s && svn merge --revision %s:%s %s . 2>>%s" % \
106            (workingdir, start_rev, end_rev, from_url, logfile))
107        output = pipe.readlines()
108        # FIXME: check pipe.close for errors
109        results = []
110        for line in output:
111            if line.startswith("Skipped"):
112                # This kind of conflict requires special handling.
113                filename = skipped_regex.findall(line)[0]
114                status = "C"
115            else:
116                assert line[4] == ' ', "Unexpected output from svn merge " \
117                    "operation; the 5th character should always be a space." \
118                    "  Output was %r." % line
119                filename = line[5:-1] # (strip trailing newline)
120                status = line[:4].rstrip()
121            results.append((status, filename))
122        return results
123
124    def conflicts_from_merge_results(self, results):
125        """Given the output from merge, return a list of files that had
126        conflicts."""
127        conflicts = [filename for status, filename in results if 'C' in status]
128        return conflicts
129
130    def commit(self, workingdir, commit_message, logfile):
131        """Returns newly committed revision number, or None if there was
132        nothing to commit.  -1 on error."""
133        svncmd = "cd %s && svn commit --no-auth-cache --username=mergebot " \
134            "--password=mergebot -m %s 2>>%s" % (workingdir,
135                shell_quote(commit_message), logfile)
136        return self._svn_new_rev_command(svncmd)
137
138    def _svn_new_rev_command(self, svncmd):
139        """Given an svn command that results in a new revision, return the
140        revision number, or -1 on error."""
141        pipe = os.popen(svncmd)
142        output = pipe.read()
143        retval = pipe.close()
144        if retval:
145            new_revision = -1
146        else:
147            new_revisions = re.compile("Committed revision ([0-9]+)\\.",
148                re.M).findall(output)
149            if new_revisions:
150                new_revision = new_revisions[0]
151            else:
152                new_revision = None
153        return new_revision
154
155# vim:foldcolumn=4 foldmethod=indent
156# vim:tabstop=4 shiftwidth=4 softtabstop=4 expandtab
Note: See TracBrowser for help on using the repository browser.