~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.2.3 by Michael Ellerman
Factor out bzrisms.
7
from errors import CommandError
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.80 by Michael Ellerman
After extensive user testing, ie. me using it, I've decided --pick should be
133
    def unshelve(self, patch_source, all_hunks=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)
160
        except CommandError:
161
            raise CommandError("Your shelved patch no " \
162
                    "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
163
0.1.84 by Michael Ellerman
Backup the patch when we unshelve. Suggested by Christian Reis.
164
        # Backup the shelved patch
165
        os.rename(patch_name, '%s~' % patch_name)
166
167
        if len(to_remain) > 0:
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
168
            f = open(patch_name, 'w')
169
            for hunk in to_remain:
170
                f.write(str(hunk))
0.1.58 by Michael Ellerman
Unshelve --pick was broken, because we deleted the whole patch, even when only
171
            f.close()
0.1.35 by Michael Ellerman
Use DiffStat rather than calling out to /bin/diffstat
172
0.1.80 by Michael Ellerman
After extensive user testing, ie. me using it, I've decided --pick should be
173
    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.
174
        self._check_upgrade()
0.2.8 by Michael Ellerman
Always set shelf message and print it when shelving.
175
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
176
        hunks = patch_source.readhunks()
0.1.31 by Michael Ellerman
- Keep our branch around, and use it directly instead of bzr_root.
177
0.1.80 by Michael Ellerman
After extensive user testing, ie. me using it, I've decided --pick should be
178
        if all_hunks:
179
            to_shelve = hunks
180
        else:
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
181
            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
182
0.1.58 by Michael Ellerman
Unshelve --pick was broken, because we deleted the whole patch, even when only
183
        if len(to_shelve) == 0:
0.2.3 by Michael Ellerman
Factor out bzrisms.
184
            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
185
0.2.8 by Michael Ellerman
Always set shelf message and print it when shelving.
186
        if message is None:
187
            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
188
            message = "Changes shelved on %s" % timestamp
189
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
190
        patch_name = self.next_patch()
0.2.25 by Michael Ellerman
Fixup shelving message
191
        self.log('Shelving to %s/%s: "%s"\n' % \
0.2.14 by Michael Ellerman
Use new log function.
192
                (self.name, os.path.basename(patch_name), message))
0.2.8 by Michael Ellerman
Always set shelf message and print it when shelving.
193
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
194
        patch = open(patch_name, 'a')
0.2.8 by Michael Ellerman
Always set shelf message and print it when shelving.
195
196
        assert '\n' not in message
0.2.12 by Michael Ellerman
Try to clear up terminology confusion. A shelf contains multiple patches, each
197
        patch.write("%s%s\n" % (self.MESSAGE_PREFIX, message))
198
199
        for hunk in to_shelve:
200
            patch.write(str(hunk))
201
202
        patch.flush()
203
        os.fsync(patch.fileno())
204
        patch.close()
0.1.27 by Michael Ellerman
Move all shelf functions into a class. Only logic change is we save the
205
0.1.75 by Michael Ellerman
Update for -p1 format diffs, steal some of Aaron's run_patch() from bzrtools.
206
        self._run_patch(to_shelve, reverse=True)
207
0.1.77 by Michael Ellerman
When unshelving, try to patch with --dry-run first, if that fails bail out.
208
    def _run_patch(self, patches, reverse=False, dry_run=False):
0.1.75 by Michael Ellerman
Update for -p1 format diffs, steal some of Aaron's run_patch() from bzrtools.
209
        args = ['patch', '-d', self.base, '-s', '-p1', '-f']
210
        if reverse:
211
            args.append('-R')
0.1.77 by Michael Ellerman
When unshelving, try to patch with --dry-run first, if that fails bail out.
212
        if dry_run:
213
            args.append('--dry-run')
0.1.88 by Michael Ellerman
Discard errors from patch if we're dry-running. It lies anyway.
214
            stdout = stderr = subprocess.PIPE
215
        else:
216
            stdout = stderr = None
0.1.75 by Michael Ellerman
Update for -p1 format diffs, steal some of Aaron's run_patch() from bzrtools.
217
0.1.88 by Michael Ellerman
Discard errors from patch if we're dry-running. It lies anyway.
218
        process = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=stdout,
219
                        stderr=stderr)
0.1.75 by Michael Ellerman
Update for -p1 format diffs, steal some of Aaron's run_patch() from bzrtools.
220
        for patch in patches:
221
            process.stdin.write(str(patch))
0.1.88 by Michael Ellerman
Discard errors from patch if we're dry-running. It lies anyway.
222
223
        process.communicate()
0.1.75 by Michael Ellerman
Update for -p1 format diffs, steal some of Aaron's run_patch() from bzrtools.
224
225
        result = process.wait()
226
        if result != 0:
227
            raise CommandError("Failed applying patches!")
228
229
        return result
0.1.87 by Michael Ellerman
Add support for detecting and upgrading from old format shelves.
230
231
    def _check_upgrade(self):
232
        if len(self._list_old_shelves()) > 0:
233
            raise CommandError("Old format shelves found, either upgrade " \
234
                    "or remove them!")
235
236
    def _list_old_shelves(self):
237
        import glob
238
        stem = os.path.join(self.base, '.bzr-shelf')
239
240
        patches = glob.glob(stem)
241
        patches.extend(glob.glob(stem + '-*[!~]'))
242
243
        if len(patches) == 0:
244
            return []
245
246
        def patch_index(name):
247
            if name == stem:
248
                return 0
249
            return int(name[len(stem) + 1:])
250
251
        # patches might not be sorted in the right order
252
        patch_ids = []
253
        for patch in patches:
254
            if patch == stem:
255
                patch_ids.append(0)
256
            else:
257
                patch_ids.append(int(patch[len(stem) + 1:]))
258
259
        patch_ids.sort()
260
261
        patches = []
262
        for id in patch_ids:
263
            if id == 0:
264
                patches.append(stem)
265
            else:
266
                patches.append('%s-%s' % (stem, id))
267
268
        return patches
269
270
    def upgrade(self):
271
        patches = self._list_old_shelves()
272
273
        if len(patches) == 0:
274
            self.log('No old-style shelves found to upgrade.\n')
275
            return
276
277
        for patch in patches:
278
            old_file = open(patch, 'r')
279
            new_path = self.next_patch()
280
            new_file = open(new_path, 'w')
281
            new_file.write(old_file.read())
282
            old_file.close()
283
            new_file.close()
284
            self.log('Copied %s to %s/%s\n' % (os.path.basename(patch),
285
                self.name, os.path.basename(new_path)))
286
            os.rename(patch, patch + '~')