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

Revision 158, 9.7 KB checked in by thomas, 6 years ago (diff)
  • moap/command/cl.py (Checkin, Diff, Prepare): Update summary and description.
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    summary = "prepare ChangeLog entry from local diff"
174    description = """This command prepares a new ChangeLog entry by analyzing
175the local changes.
176It uses ctags to extract the tags affected by the changes, and adds them
177to the ChangeLog entries.
178It decides your name based on your account settings, the REAL_NAME or
179CHANGE_LOG_NAME environment variables.
180It decides your e-mail address based on the EMAIL_ADDRESS environment
181variable.
182"""
183    usage = "prepare [path to directory or ChangeLog file]"
184    aliases = ["pr", "prep", ]
185
186    def do(self, args):
187        clPath = "ChangeLog"
188        if args:
189            clPath = args[0]
190
191        vcsPath = os.path.dirname(os.path.abspath(clPath))
192        v = vcs.detect(vcsPath)
193        if not v:
194            self.stderr.write('No VCS detected in %s\n' % vcsPath)
195            return 3
196
197        self.stdout.write('Updating %s from %s repository.\n' % (clPath,
198            v.name))
199        try:
200            v.update(clPath)
201        except vcs.VCSException, e:
202            self.stderr.write('Could not update %s:\n%s\n' % (clPath, e.args))
203            return 3
204
205        self.stdout.write('Finding changes.\n')
206        changes = v.getChanges(vcsPath)
207
208        # filter out the ChangeLog we're preparing
209        if os.path.abspath(clPath) in changes.keys():
210            del changes[os.path.abspath(clPath)]
211
212        if not changes:
213            self.stdout.write('No changes detected.\n')
214            return 0
215
216        files = changes.keys()
217        files.sort()
218
219        # get the tags for all the files we're looking at
220        ct = ctags.CTags()
221        binary = None
222        for candidate in ["ctags", "exuberant-ctags"]:
223            if os.system('which %s > /dev/null 2>&1' % candidate) == 0:
224                binary = candidate
225                break
226        if not binary:
227            self.stderr.write(
228                'No ctags binary found, consider installing it to get '
229                'changes per tag.\n')
230        else:
231            self.stdout.write('Extracting affected tags from source.\n')
232            command = "%s -u --fields=+nlS -f - %s" % (binary, " ".join(files))
233            self.debug('Running command %s' % command)
234            output = commands.getoutput(command)
235            ct.addString(output)
236
237        # prepare header for entry
238        date = time.strftime('%Y-%m-%d')
239        for name in [
240            os.environ.get('CHANGE_LOG_NAME'),
241            os.environ.get('REAL_NAME'),
242            pwd.getpwuid(os.getuid()).pw_gecos,
243            "Please set CHANGE_LOG_NAME or REAL_NAME environment variable"]:
244            if name:
245                break
246
247        for mail in [
248            os.environ.get('EMAIL_ADDRESS'),
249            "Please set EMAIL_ADDRESS environment variable"]:
250            if mail:
251                break
252
253        self.stdout.write('Editing %s.\n' % clPath)
254        (fd, path) = tempfile.mkstemp(suffix='.moap')
255        os.write(fd, "%s  %s  <%s>\n\n" % (date, name, mail))
256        os.write(fd, "\treviewed by: <delete if not using a buddy>\n");
257        os.write(fd, "\tpatch by: <delete if not someone else's patch>\n");
258        os.write(fd, "\n")
259
260        for filePath in files:
261            lines = changes[filePath]
262            tags = []
263            for oldLine, oldCount, newLine, newCount in lines:
264                self.log("Looking in file %s, newLine %r, newCount %r" % (
265                    filePath, newLine, newCount))
266                try:
267                    for t in ct.getTags(filePath, newLine, newCount):
268                        # we want unique tags, not several hits for one
269                        if not t in tags:
270                            tags.append(t)
271                except KeyError:
272                    pass
273
274            # the paths are absolute because we asked for an absolute path diff
275            # strip them to be relative
276            if filePath.startswith(vcsPath):
277                filePath = filePath[len(vcsPath) + 1:]
278            tagPart = ""
279            if tags:
280                parts = []
281                for tag in tags:
282                    if tag.klazz:
283                        parts.append('%s.%s' % (tag.klazz, tag.name))
284                    else:
285                        parts.append(tag.name)
286                tagPart = " (" + ", ".join(parts) + ")"
287            line =  "\t* %s%s:\n" % (filePath, tagPart)
288            # wrap to maximum 72 characters, and keep tabs
289            lines = textwrap.wrap(line, 72, expand_tabs=False,
290                replace_whitespace=False,
291                subsequent_indent="\t  ")
292            os.write(fd, "\n".join(lines) + '\n')
293
294        os.write(fd, "\n")
295
296        # copy rest of ChangeLog file
297        handle = open(clPath)
298        while True:
299            data = handle.read()
300            if not data:
301                break
302            os.write(fd, data)
303        os.close(fd)
304        # FIXME: figure out a nice pythonic move for cross-device links instead
305        os.system("mv %s %s" % (path, clPath))
306
307        return 0
308
309class ChangeLog(util.LogCommand):
310    usage = "changelog %command"
311
312    description = "act on ChangeLog file"
313    subCommandClasses = [Checkin, Diff, Prepare]
314    aliases = ["cl", ]
315
Note: See TracBrowser for help on using the repository browser.