~abentley/bzrtools/bzrtools.dev

0.1.1 by Michael Ellerman
Initial import
1
#!/usr/bin/python
2
3
import os
4
import sys
0.1.75 by Michael Ellerman
Update for -p1 format diffs, steal some of Aaron's run_patch() from bzrtools.
5
import subprocess
0.1.87 by Michael Ellerman
Add support for detecting and upgrading from old format shelves.
6
from datetime import datetime
0.1.89 by Michael Ellerman
Add support for unshelving -p0 patches, for backward compatibility.
7
from errors import CommandError, PatchFailed
0.1.56 by Michael Ellerman
Make HunkSelector agnostic as to whether it's selecting for shelving or
8
from hunk_selector import ShelveHunkSelector, UnshelveHunkSelector
0.2.1 by Michael Ellerman
Factor out patch generation into PatchSource classes.
9
from patchsource import PatchSource, FilePatchSource
0.1.23 by Michael Ellerman
Incorporate Aaron's changes from bzrtools.
10
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
11
class Shelf(object):
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
12
    MESSAGE_PREFIX = "# Shelved patch: "
13
0.2.22 by Michael Ellerman
Infrastructure to allow for multiple interchangeable shelves.
14
    _paths = {
15
        'base'          : '.shelf',
16
        'shelves'       : '.shelf/shelves',
17
        'current-shelf' : '.shelf/current-shelf',
18
    }
19
20
    def __init__(self, base, name=None):
21
        self.base = base
22
        self.__setup()
23
24
        if name is None:
25
            current = os.path.join(self.base, self._paths['current-shelf'])
26
            name = open(current).read().strip()
27
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
28
        assert '\n' not in name
0.2.5 by Michael Ellerman
Make some of the messages a bit more readable.
29
        self.name = name
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
30
0.2.22 by Michael Ellerman
Infrastructure to allow for multiple interchangeable shelves.
31
        self.dir = os.path.join(self.base, self._paths['shelves'], name)
32
        if not os.path.isdir(self.dir):
33
            os.mkdir(self.dir)
0.1.43 by Michael Ellerman
New shelf layout. Shelves now sit under .bzr/x-shelf/default/
34
0.2.22 by Michael Ellerman
Infrastructure to allow for multiple interchangeable shelves.
35
    def __setup(self):
36
        # Create required directories etc.
37
        for dir in [self._paths['base'], self._paths['shelves']]:
38
            dir = os.path.join(self.base, dir)
0.2.2 by Michael Ellerman
For the moment at least storing scads of stuff under .bzr isn't really
39
            if not os.path.isdir(dir):
40
                os.mkdir(dir)
0.1.43 by Michael Ellerman
New shelf layout. Shelves now sit under .bzr/x-shelf/default/
41
0.2.22 by Michael Ellerman
Infrastructure to allow for multiple interchangeable shelves.
42
        current = os.path.join(self.base, self._paths['current-shelf'])
43
        if not os.path.exists(current):
44
            f = open(current, 'w')
45
            f.write('default')
46
            f.close()
47
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
48
    def make_default(self):
49
        f = open(os.path.join(self.base, self._paths['current-shelf']), 'w')
50
        f.write(self.name)
51
        f.close()
52
        self.log("Default shelf is now '%s'\n" % self.name)
53
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
54
    def log(self, msg):
0.2.20 by Michael Ellerman
Make list output look nicer when the shelf is empty.
55
        sys.stderr.write(msg)
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
56
0.2.19 by Michael Ellerman
Add shelf delete subcommand, make list barf if it gets an arg.
57
    def delete(self, patch):
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
58
        path = self.__path_from_user(patch)
0.2.19 by Michael Ellerman
Add shelf delete subcommand, make list barf if it gets an arg.
59
        os.remove(path)
60
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
61
    def display(self, patch):
62
        path = self.__path_from_user(patch)
63
        sys.stdout.write(open(path).read())
64
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
65
    def list(self):
0.1.83 by Michael Ellerman
Cope if there's bogus files in the shelf directory.
66
        indexes = self.__list()
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
67
        self.log("Patches on shelf '%s':" % self.name)
0.2.20 by Michael Ellerman
Make list output look nicer when the shelf is empty.
68
        if len(indexes) == 0:
69
            self.log(' None\n')
70
            return
71
        self.log('\n')
72
        for index in indexes:
0.2.18 by Michael Ellerman
Make next_patch() cope with holes, eg. 00, 02 etc.
73
            msg = self.get_patch_message(self.__path(index))
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
74
            if msg is None:
75
                msg = "No message saved with patch."
0.2.20 by Michael Ellerman
Make list output look nicer when the shelf is empty.
76
            self.log(' %.2d: %s\n' % (index, msg))
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
77
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
78
    def __path_from_user(self, patch_id):
79
        try:
0.2.29 by Michael Ellerman
Better error message when trying to show an non-existant patch
80
            patch_index = int(patch_id)
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
81
        except TypeError:
82
            raise CommandError("Invalid patch name '%s'" % patch_id)
83
0.2.29 by Michael Ellerman
Better error message when trying to show an non-existant patch
84
        path = self.__path(patch_index)
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
85
86
        if not os.path.exists(path):
0.2.29 by Michael Ellerman
Better error message when trying to show an non-existant patch
87
            raise CommandError("Patch '%s' doesn't exist on shelf %s!" % \
88
                        (patch_id, self.name))
0.2.24 by Michael Ellerman
Add switch command to switch between multiple shelves.
89
90
        return path
91
0.2.18 by Michael Ellerman
Make next_patch() cope with holes, eg. 00, 02 etc.
92
    def __path(self, index):
0.2.22 by Michael Ellerman
Infrastructure to allow for multiple interchangeable shelves.
93
        return os.path.join(self.dir, '%.2d' % index)
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
94
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
95
    def next_patch(self):
0.2.18 by Michael Ellerman
Make next_patch() cope with holes, eg. 00, 02 etc.
96
        indexes = self.__list()
97
98
        if len(indexes) == 0:
99
            next = 0
100
        else:
101
            next = indexes[-1] + 1
102
        return self.__path(next)
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
103
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
104
    def __list(self):
0.2.22 by Michael Ellerman
Infrastructure to allow for multiple interchangeable shelves.
105
        patches = os.listdir(self.dir)
0.1.83 by Michael Ellerman
Cope if there's bogus files in the shelf directory.
106
        indexes = []
107
        for f in patches:
108
            if f.endswith('~'):
109
                continue # ignore backup files
110
            try:
111
                indexes.append(int(f))
112
            except ValueError:
113
                self.log("Warning: Ignoring junk file '%s' on shelf.\n" % f)
114
0.2.18 by Michael Ellerman
Make next_patch() cope with holes, eg. 00, 02 etc.
115
        indexes.sort()
116
        return indexes
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
117
118
    def last_patch(self):
0.2.18 by Michael Ellerman
Make next_patch() cope with holes, eg. 00, 02 etc.
119
        indexes = self.__list()
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
120
0.2.18 by Michael Ellerman
Make next_patch() cope with holes, eg. 00, 02 etc.
121
        if len(indexes) == 0:
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
122
            return None
123
0.2.18 by Michael Ellerman
Make next_patch() cope with holes, eg. 00, 02 etc.
124
        return self.__path(indexes[-1])
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
125
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
126
    def get_patch_message(self, patch_path):
127
        patch = open(patch_path, 'r').read()
128
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
129
        if not patch.startswith(self.MESSAGE_PREFIX):
130
            return None
131
        return patch[len(self.MESSAGE_PREFIX):patch.index('\n')]
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
132
0.1.91 by Michael Ellerman
Add --force option to unshelve, which runs the shelved changes through
133
    def unshelve(self, patch_source, all_hunks=False, force=False):
0.1.87 by Michael Ellerman
Add support for detecting and upgrading from old format shelves.
134
        self._check_upgrade()
135
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
136
        patch_name = self.last_patch()
137
138
        if patch_name is None:
139
            raise CommandError("No patch found on shelf %s" % self.name)
140
141
        hunks = FilePatchSource(patch_name).readhunks()
0.1.80 by Michael Ellerman
After extensive user testing, ie. me using it, I've decided --pick should be
142
        if all_hunks:
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
143
            to_unshelve = hunks
0.1.58 by Michael Ellerman
Unshelve --pick was broken, because we deleted the whole patch, even when only
144
            to_remain = []
0.1.80 by Michael Ellerman
After extensive user testing, ie. me using it, I've decided --pick should be
145
        else:
146
            to_unshelve, to_remain = UnshelveHunkSelector(hunks).select()
0.1.55 by Michael Ellerman
Add support for 'unshelve --pick'. This works but the UI is broken, as the
147
0.1.58 by Michael Ellerman
Unshelve --pick was broken, because we deleted the whole patch, even when only
148
        if len(to_unshelve) == 0:
0.2.3 by Michael Ellerman
Factor out bzrisms.
149
            raise CommandError('Nothing to unshelve')
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
150
0.2.13 by Michael Ellerman
Add shelf command with subcommand "list" which lists the shelf contents.
151
        message = self.get_patch_message(patch_name)
0.2.14 by Michael Ellerman
Use new log function.
152
        if message is None:
0.1.78 by Michael Ellerman
I'm sure someone will complain about this, but remove the diffstat after
153
            message = "No message saved with patch."
154
        self.log('Unshelving from %s/%s: "%s"\n' % \
155
                (self.name, os.path.basename(patch_name), message))
0.1.58 by Michael Ellerman
Unshelve --pick was broken, because we deleted the whole patch, even when only
156
0.1.76 by Michael Ellerman
Add a test to make sure we don't delete the shelved patch if unshelving
157
        try:
0.1.77 by Michael Ellerman
When unshelving, try to patch with --dry-run first, if that fails bail out.
158
            self._run_patch(to_unshelve, dry_run=True)
0.1.76 by Michael Ellerman
Add a test to make sure we don't delete the shelved patch if unshelving
159
            self._run_patch(to_unshelve)
0.1.89 by Michael Ellerman
Add support for unshelving -p0 patches, for backward compatibility.
160
        except PatchFailed:
161
            try:
162
                self._run_patch(to_unshelve, strip=0, dry_run=True)
163
                self._run_patch(to_unshelve, strip=0)
164
            except PatchFailed:
0.1.91 by Michael Ellerman
Add --force option to unshelve, which runs the shelved changes through
165
                if force:
166
                    self.log('Warning: Unshelving failed, forcing as ' \
167
                             'requested. Shelf will not be modified.\n')
168
                    try:
169
                        self._run_patch(to_unshelve)
170
                    except PatchFailed:
171
                        pass
172
                    return
0.1.89 by Michael Ellerman
Add support for unshelving -p0 patches, for backward compatibility.
173
                raise CommandError("Your shelved patch no " \
0.1.76 by Michael Ellerman
Add a test to make sure we don't delete the shelved patch if unshelving
174
                    "longer applies cleanly to the working tree!")
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
175
0.1.84 by Michael Ellerman
Backup the patch when we unshelve. Suggested by Christian Reis.
176
        # Backup the shelved patch
177
        os.rename(patch_name, '%s~' % patch_name)
178
179
        if len(to_remain) > 0:
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
180
            f = open(patch_name, 'w')
181
            for hunk in to_remain:
182
                f.write(str(hunk))
0.1.58 by Michael Ellerman
Unshelve --pick was broken, because we deleted the whole patch, even when only
183
            f.close()
0.1.35 by Michael Ellerman
Use DiffStat rather than calling out to /bin/diffstat
184
0.1.80 by Michael Ellerman
After extensive user testing, ie. me using it, I've decided --pick should be
185
    def shelve(self, patch_source, all_hunks=False, message=None):
0.1.87 by Michael Ellerman
Add support for detecting and upgrading from old format shelves.
186
        self._check_upgrade()
0.2.8 by Michael Ellerman
Always set shelf message and print it when shelving.
187
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
188
        hunks = patch_source.readhunks()
0.1.31 by Michael Ellerman
- Keep our branch around, and use it directly instead of bzr_root.
189
0.1.80 by Michael Ellerman
After extensive user testing, ie. me using it, I've decided --pick should be
190
        if all_hunks:
191
            to_shelve = hunks
192
        else:
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
193
            to_shelve = ShelveHunkSelector(hunks).select()[0]
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
194
0.1.58 by Michael Ellerman
Unshelve --pick was broken, because we deleted the whole patch, even when only
195
        if len(to_shelve) == 0:
0.2.3 by Michael Ellerman
Factor out bzrisms.
196
            raise CommandError('Nothing to shelve')
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
197
0.2.8 by Michael Ellerman
Always set shelf message and print it when shelving.
198
        if message is None:
199
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
200
            message = "Changes shelved on %s" % timestamp
201
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
202
        patch_name = self.next_patch()
0.2.25 by Michael Ellerman
Fixup shelving message
203
        self.log('Shelving to %s/%s: "%s"\n' % \
0.2.14 by Michael Ellerman
Use new log function.
204
                (self.name, os.path.basename(patch_name), message))
0.2.8 by Michael Ellerman
Always set shelf message and print it when shelving.
205
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
206
        patch = open(patch_name, 'a')
0.2.8 by Michael Ellerman
Always set shelf message and print it when shelving.
207
208
        assert '\n' not in message
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
209
        patch.write("%s%s\n" % (self.MESSAGE_PREFIX, message))
210
211
        for hunk in to_shelve:
212
            patch.write(str(hunk))
213
214
        patch.flush()
215
        os.fsync(patch.fileno())
216
        patch.close()
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
217
0.1.94 by Michael Ellerman
Support for 0.7 format diffs when shelving, and better error handling
218
        try:
219
            self._run_patch(to_shelve, reverse=True, dry_run=True)
220
            self._run_patch(to_shelve, reverse=True)
221
        except PatchFailed:
222
            try:
223
                self._run_patch(to_shelve, reverse=True, strip=0, dry_run=True)
224
                self._run_patch(to_shelve, reverse=True, strip=0)
225
            except PatchFailed:
226
                raise CommandError("Failed removing shelved changes from the"
227
                    "working tree!")
0.1.75 by Michael Ellerman
Update for -p1 format diffs, steal some of Aaron's run_patch() from bzrtools.
228
0.1.89 by Michael Ellerman
Add support for unshelving -p0 patches, for backward compatibility.
229
    def _run_patch(self, patches, strip=1, reverse=False, dry_run=False):
230
        args = ['patch', '-d', self.base, '-s', '-p%d' % strip, '-f']
0.1.75 by Michael Ellerman
Update for -p1 format diffs, steal some of Aaron's run_patch() from bzrtools.
231
        if reverse:
232
            args.append('-R')
0.1.77 by Michael Ellerman
When unshelving, try to patch with --dry-run first, if that fails bail out.
233
        if dry_run:
234
            args.append('--dry-run')
0.1.88 by Michael Ellerman
Discard errors from patch if we're dry-running. It lies anyway.
235
            stdout = stderr = subprocess.PIPE
236
        else:
237
            stdout = stderr = None
0.1.75 by Michael Ellerman
Update for -p1 format diffs, steal some of Aaron's run_patch() from bzrtools.
238
0.1.88 by Michael Ellerman
Discard errors from patch if we're dry-running. It lies anyway.
239
        process = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=stdout,
240
                        stderr=stderr)
0.1.75 by Michael Ellerman
Update for -p1 format diffs, steal some of Aaron's run_patch() from bzrtools.
241
        for patch in patches:
242
            process.stdin.write(str(patch))
0.1.88 by Michael Ellerman
Discard errors from patch if we're dry-running. It lies anyway.
243
244
        process.communicate()
0.1.75 by Michael Ellerman
Update for -p1 format diffs, steal some of Aaron's run_patch() from bzrtools.
245
246
        result = process.wait()
247
        if result != 0:
0.1.89 by Michael Ellerman
Add support for unshelving -p0 patches, for backward compatibility.
248
            raise PatchFailed()
0.1.75 by Michael Ellerman
Update for -p1 format diffs, steal some of Aaron's run_patch() from bzrtools.
249
250
        return result
0.1.87 by Michael Ellerman
Add support for detecting and upgrading from old format shelves.
251
252
    def _check_upgrade(self):
253
        if len(self._list_old_shelves()) > 0:
254
            raise CommandError("Old format shelves found, either upgrade " \
255
                    "or remove them!")
256
257
    def _list_old_shelves(self):
258
        import glob
259
        stem = os.path.join(self.base, '.bzr-shelf')
260
261
        patches = glob.glob(stem)
262
        patches.extend(glob.glob(stem + '-*[!~]'))
263
264
        if len(patches) == 0:
265
            return []
266
267
        def patch_index(name):
268
            if name == stem:
269
                return 0
270
            return int(name[len(stem) + 1:])
271
272
        # patches might not be sorted in the right order
273
        patch_ids = []
274
        for patch in patches:
275
            if patch == stem:
276
                patch_ids.append(0)
277
            else:
278
                patch_ids.append(int(patch[len(stem) + 1:]))
279
280
        patch_ids.sort()
281
282
        patches = []
283
        for id in patch_ids:
284
            if id == 0:
285
                patches.append(stem)
286
            else:
287
                patches.append('%s-%s' % (stem, id))
288
289
        return patches
290
291
    def upgrade(self):
292
        patches = self._list_old_shelves()
293
294
        if len(patches) == 0:
295
            self.log('No old-style shelves found to upgrade.\n')
296
            return
297
298
        for patch in patches:
299
            old_file = open(patch, 'r')
300
            new_path = self.next_patch()
301
            new_file = open(new_path, 'w')
302
            new_file.write(old_file.read())
303
            old_file.close()
304
            new_file.close()
305
            self.log('Copied %s to %s/%s\n' % (os.path.basename(patch),
306
                self.name, os.path.basename(new_path)))
307
            os.rename(patch, patch + '~')