source: trunk/moap/command/cl.py @ 152

Revision 152, 9.3 KB checked in by thomas, 6 years ago (diff)
Line 
1# -*- Mode: Python; test-case-name: moap.test.test_commands_cl -*-
2# vi:si:et:sw=4:sts=4:ts=4
3
4import commands
5import os
6import pwd
7import time
8import re
9import tempfile
10import textwrap
11
12from moap.util import log, util, ctags
13from moap.vcs import vcs
14
15description = "Read and act on ChangeLog"
16
17# for matching the first line of an entry
18_nameRegex = re.compile('^(\d*-\d*-\d*)\s*(.*)$')
19
20# for matching the address out of the second part of the first line
21_addressRegex = re.compile('^([^<]*)<(.*)>$')
22# for matching files changed
23_fileRegex = re.compile('^\s*\* (.[^:\s\(]*).*')
24
25class Entry:
26    """
27    I represent one entry in a ChangeLog file.
28
29    @ivar text:    the text of the message, without name line or
30                   preceding/following newlines
31    @type text:    str
32    @type date:    str
33    @type name:    str
34    @type address: str
35    @ivar files:   list of files referenced in this ChangeLog entry
36    @type files:   list of str
37    """
38    date = None
39    name = None
40    address = None
41
42    def __init__(self):
43        self.files = []
44
45    def parse(self, lines):
46        """
47        @type lines: list of str
48        """
49        # first line is the "name" line
50        m = _nameRegex.search(lines[0])
51        self.date = m.expand("\\1")
52        self.name = m.expand("\\2")
53        m = _addressRegex.search(self.name)
54        if m:
55            self.name = m.expand("\\1").strip()
56            self.address = m.expand("\\2")
57
58        # all the other lines can contain files
59        for line in lines[1:]:
60            m = _fileRegex.search(line)
61            if m:
62                fileName = m.expand("\\1")
63                self.files.append(fileName)
64
65        # create the text attribute
66        save = []
67        for line in lines[1:]:
68            line = line.rstrip()
69            if len(line) > 0:
70                save.append(line)
71        self.text = "\n".join(save) + "\n"
72
73class ChangeLogFile(log.Loggable):
74    logCategory = "ChangeLog"
75
76    def __init__(self, path):
77        self._path = path
78        self._blocks = []
79        self._entries = []
80        handle = open(path, "r")
81
82        def parseBlock(block):
83            if block:
84                self._blocks.append(block)
85                entry = Entry()
86                entry.parse(block)
87                self._entries.append(entry)
88
89        block = []
90        for line in handle.readlines():
91            if _nameRegex.match(line):
92                # new entry starting, save old block
93                parseBlock(block)
94
95                block = []
96            block.append(line)
97
98        # don't forget the last block
99        parseBlock(block)
100
101        self.debug('%d entries in %s' % (len(self._entries), path))
102
103    def getEntry(self, num):
104        """
105        Get the nth entry from the ChangeLog, starting from 0 for the most
106        recent one.
107
108        @raises: IndexError
109        """
110        return self._entries[num]
111
112class Checkin(util.LogCommand):
113    description = "Check in files listed in the latest ChangeLog entry"
114    usage = "checkin [path to directory or ChangeLog file]"
115    aliases = ["ci", ]
116
117    def do(self, args):
118        path = os.getcwd()
119        if args:
120            path = os.path.abspath(args[0])
121         
122        self.debug('changelog checkin: path %s' % path)
123        if os.path.isdir(path):
124            filePath = os.path.join(path, 'ChangeLog')
125        else:
126            filePath = path
127        fileName = os.path.basename(filePath)
128        fileDir = os.path.dirname(filePath)
129        if not os.path.exists(filePath):
130            self.stderr.write('No %s found in %s.\n' % (fileName, fileDir))
131            return 3
132
133        v = vcs.detect(fileDir)
134        if not v:
135            self.stderr.write('No VCS detected in %s\n' % fileDir)
136            return 3
137
138        cl = ChangeLogFile(filePath)
139        # get latest entry
140        entry = cl.getEntry(0)
141        ret = v.commit([fileName, ] + entry.files, entry.text)
142        if not ret:
143            return 1
144
145        return 0
146
147class Diff(util.LogCommand):
148    description = "Show diff for all files from last ChangeLog entry"
149
150    def do(self, args):
151        path = os.getcwd()
152        if args:
153            path = args[0]
154         
155        fileName = os.path.join(path, 'ChangeLog')
156        if not os.path.exists(fileName):
157            self.stderr.write('No ChangeLog found in %s.\n' % path)
158            return 3
159
160        v = vcs.detect(path)
161        if not v:
162            self.stderr.write('No VCS detected in %s\n' % path)
163            return 3
164
165        cl = ChangeLogFile(fileName)
166        # get latest entry
167        entry = cl.getEntry(0)
168        for fileName in entry.files:
169            self.debug('diffing %s' % fileName)
170            print v.diff(fileName)
171
172class Prepare(util.LogCommand):
173    description = "Prepares ChangeLog entry from local diff"
174    aliases = ["pr", "prep", ]
175
176    def do(self, args):
177        clPath = "ChangeLog"
178        if args:
179            clPath = args[0]
180
181        vcsPath = os.path.dirname(os.path.abspath(clPath))
182        v = vcs.detect(vcsPath)
183        if not v:
184            self.stderr.write('No VCS detected in %s\n' % vcsPath)
185            return 3
186
187        self.stdout.write('Updating %s from %s repository.\n' % (clPath,
188            v.name))
189        try:
190            v.update(clPath)
191        except vcs.VCSException, e:
192            self.stderr.write('Could not update %s:\n%s\n' % (clPath, e.args))
193            return 3
194
195        self.stdout.write('Finding changes.\n')
196        changes = v.getChanges(vcsPath)
197
198        # filter out the ChangeLog we're preparing
199        if os.path.abspath(clPath) in changes.keys():
200            del changes[os.path.abspath(clPath)]
201
202        if not changes:
203            self.stdout.write('No changes detected.\n')
204            return 0
205
206        files = changes.keys()
207        files.sort()
208
209        # get the tags for all the files we're looking at
210        ct = ctags.CTags()
211        binary = None
212        for candidate in ["ctags", "exuberant-ctags"]:
213            if os.system('which %s > /dev/null 2>&1' % candidate) == 0:
214                binary = candidate
215                break
216        if not binary:
217            self.stderr.write(
218                'No ctags binary found, consider installing it to get '
219                'changes per tag.\n')
220        else:
221            self.stdout.write('Extracting affected tags from source.\n')
222            command = "%s -u --fields=+nlS -f - %s" % (binary, " ".join(files))
223            self.debug('Running command %s' % command)
224            output = commands.getoutput(command)
225            ct.addString(output)
226
227        # prepare header for entry
228        date = time.strftime('%Y-%m-%d')
229        for name in [
230            os.environ.get('CHANGE_LOG_NAME'),
231            os.environ.get('REAL_NAME'),
232            pwd.getpwuid(os.getuid()).pw_gecos,
233            "Please set CHANGE_LOG_NAME or REAL_NAME environment variable"]:
234            if name:
235                break
236
237        for mail in [
238            os.environ.get('EMAIL_ADDRESS'),
239            "Please set EMAIL_ADDRESS environment variable"]:
240            if mail:
241                break
242
243        self.stdout.write('Editing %s.\n' % clPath)
244        (fd, path) = tempfile.mkstemp(suffix='.moap')
245        os.write(fd, "%s  %s  <%s>\n\n" % (date, name, mail))
246        os.write(fd, "\treviewed by: <delete if not using a buddy>\n");
247        os.write(fd, "\tpatch by: <delete if not someone else's patch>\n");
248        os.write(fd, "\n")
249
250        for filePath in files:
251            lines = changes[filePath]
252            tags = []
253            for oldLine, oldCount, newLine, newCount in lines:
254                self.log("Looking in file %s, newLine %r, newCount %r" % (
255                    filePath, newLine, newCount))
256                try:
257                    for t in ct.getTags(filePath, newLine, newCount):
258                        # we want unique tags, not several hits for one
259                        if not t in tags:
260                            tags.append(t)
261                except KeyError:
262                    pass
263
264            # the paths are absolute because we asked for an absolute path diff
265            # strip them to be relative
266            if filePath.startswith(vcsPath):
267                filePath = filePath[len(vcsPath) + 1:]
268            tagPart = ""
269            if tags:
270                parts = []
271                for tag in tags:
272                    if tag.klazz:
273                        parts.append('%s.%s' % (tag.klazz, tag.name))
274                    else:
275                        parts.append(tag.name)
276                tagPart = " (" + ", ".join(parts) + ")"
277            line =  "\t* %s%s:\n" % (filePath, tagPart)
278            # wrap to maximum 72 characters, and keep tabs
279            lines = textwrap.wrap(line, 72, expand_tabs=False,
280                replace_whitespace=False,
281                subsequent_indent="\t  ")
282            os.write(fd, "\n".join(lines) + '\n')
283
284        os.write(fd, "\n")
285
286        # copy rest of ChangeLog file
287        handle = open(clPath)
288        while True:
289            data = handle.read()
290            if not data:
291                break
292            os.write(fd, data)
293        os.close(fd)
294        # FIXME: figure out a nice pythonic move for cross-device links instead
295        os.system("mv %s %s" % (path, clPath))
296
297        return 0
298
299class ChangeLog(util.LogCommand):
300    usage = "changelog %command"
301
302    description = "act on ChangeLog file"
303    subCommandClasses = [Checkin, Diff, Prepare]
304    aliases = ["cl", ]
305
Note: See TracBrowser for help on using the repository browser.