source: mergebot/trunk/mergebot/SvnOps.py @ 24

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

Ticket #2: merge to trunk

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