Assorted typos
[stgit] / stgit / stack.py
1 """Basic quilt-like functionality
2 """
3
4 __copyright__ = """
5 Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
6
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 """
20
21 import sys, os, re
22
23 from stgit.utils import *
24 from stgit import git, basedir, templates
25 from stgit.config import config
26
27
28 # stack exception class
29 class StackException(Exception):
30 pass
31
32 class FilterUntil:
33 def __init__(self):
34 self.should_print = True
35 def __call__(self, x, until_test, prefix):
36 if until_test(x):
37 self.should_print = False
38 if self.should_print:
39 return x[0:len(prefix)] != prefix
40 return False
41
42 #
43 # Functions
44 #
45 __comment_prefix = 'STG:'
46 __patch_prefix = 'STG_PATCH:'
47
48 def __clean_comments(f):
49 """Removes lines marked for status in a commit file
50 """
51 f.seek(0)
52
53 # remove status-prefixed lines
54 lines = f.readlines()
55
56 patch_filter = FilterUntil()
57 until_test = lambda t: t == (__patch_prefix + '\n')
58 lines = [l for l in lines if patch_filter(l, until_test, __comment_prefix)]
59
60 # remove empty lines at the end
61 while len(lines) != 0 and lines[-1] == '\n':
62 del lines[-1]
63
64 f.seek(0); f.truncate()
65 f.writelines(lines)
66
67 def edit_file(series, line, comment, show_patch = True):
68 fname = '.stgitmsg.txt'
69 tmpl = templates.get_template('patchdescr.tmpl')
70
71 f = file(fname, 'w+')
72 if line:
73 print >> f, line
74 elif tmpl:
75 print >> f, tmpl,
76 else:
77 print >> f
78 print >> f, __comment_prefix, comment
79 print >> f, __comment_prefix, \
80 'Lines prefixed with "%s" will be automatically removed.' \
81 % __comment_prefix
82 print >> f, __comment_prefix, \
83 'Trailing empty lines will be automatically removed.'
84
85 if show_patch:
86 print >> f, __patch_prefix
87 # series.get_patch(series.get_current()).get_top()
88 git.diff([], series.get_patch(series.get_current()).get_bottom(), None, f)
89
90 #Vim modeline must be near the end.
91 print >> f, __comment_prefix, 'vi: set textwidth=75 filetype=diff nobackup:'
92 f.close()
93
94 # the editor
95 editor = config.get('stgit.editor')
96 if editor:
97 pass
98 elif 'EDITOR' in os.environ:
99 editor = os.environ['EDITOR']
100 else:
101 editor = 'vi'
102 editor += ' %s' % fname
103
104 print 'Invoking the editor: "%s"...' % editor,
105 sys.stdout.flush()
106 print 'done (exit code: %d)' % os.system(editor)
107
108 f = file(fname, 'r+')
109
110 __clean_comments(f)
111 f.seek(0)
112 result = f.read()
113
114 f.close()
115 os.remove(fname)
116
117 return result
118
119 #
120 # Classes
121 #
122
123 class StgitObject:
124 """An object with stgit-like properties stored as files in a directory
125 """
126 def _set_dir(self, dir):
127 self.__dir = dir
128 def _dir(self):
129 return self.__dir
130
131 def create_empty_field(self, name):
132 create_empty_file(os.path.join(self.__dir, name))
133
134 def _get_field(self, name, multiline = False):
135 id_file = os.path.join(self.__dir, name)
136 if os.path.isfile(id_file):
137 line = read_string(id_file, multiline)
138 if line == '':
139 return None
140 else:
141 return line
142 else:
143 return None
144
145 def _set_field(self, name, value, multiline = False):
146 fname = os.path.join(self.__dir, name)
147 if value and value != '':
148 write_string(fname, value, multiline)
149 elif os.path.isfile(fname):
150 os.remove(fname)
151
152
153 class Patch(StgitObject):
154 """Basic patch implementation
155 """
156 def __init__(self, name, series_dir, refs_dir):
157 self.__series_dir = series_dir
158 self.__name = name
159 self._set_dir(os.path.join(self.__series_dir, self.__name))
160 self.__refs_dir = refs_dir
161 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
162 self.__log_ref_file = os.path.join(self.__refs_dir,
163 self.__name + '.log')
164
165 def create(self):
166 os.mkdir(self._dir())
167 self.create_empty_field('bottom')
168 self.create_empty_field('top')
169
170 def delete(self):
171 for f in os.listdir(self._dir()):
172 os.remove(os.path.join(self._dir(), f))
173 os.rmdir(self._dir())
174 os.remove(self.__top_ref_file)
175 if os.path.exists(self.__log_ref_file):
176 os.remove(self.__log_ref_file)
177
178 def get_name(self):
179 return self.__name
180
181 def rename(self, newname):
182 olddir = self._dir()
183 old_top_ref_file = self.__top_ref_file
184 old_log_ref_file = self.__log_ref_file
185 self.__name = newname
186 self._set_dir(os.path.join(self.__series_dir, self.__name))
187 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
188 self.__log_ref_file = os.path.join(self.__refs_dir,
189 self.__name + '.log')
190
191 os.rename(olddir, self._dir())
192 os.rename(old_top_ref_file, self.__top_ref_file)
193 if os.path.exists(old_log_ref_file):
194 os.rename(old_log_ref_file, self.__log_ref_file)
195
196 def __update_top_ref(self, ref):
197 write_string(self.__top_ref_file, ref)
198
199 def __update_log_ref(self, ref):
200 write_string(self.__log_ref_file, ref)
201
202 def update_top_ref(self):
203 top = self.get_top()
204 if top:
205 self.__update_top_ref(top)
206
207 def get_old_bottom(self):
208 return self._get_field('bottom.old')
209
210 def get_bottom(self):
211 return self._get_field('bottom')
212
213 def set_bottom(self, value, backup = False):
214 if backup:
215 curr = self._get_field('bottom')
216 self._set_field('bottom.old', curr)
217 self._set_field('bottom', value)
218
219 def get_old_top(self):
220 return self._get_field('top.old')
221
222 def get_top(self):
223 return self._get_field('top')
224
225 def set_top(self, value, backup = False):
226 if backup:
227 curr = self._get_field('top')
228 self._set_field('top.old', curr)
229 self._set_field('top', value)
230 self.__update_top_ref(value)
231
232 def restore_old_boundaries(self):
233 bottom = self._get_field('bottom.old')
234 top = self._get_field('top.old')
235
236 if top and bottom:
237 self._set_field('bottom', bottom)
238 self._set_field('top', top)
239 self.__update_top_ref(top)
240 return True
241 else:
242 return False
243
244 def get_description(self):
245 return self._get_field('description', True)
246
247 def set_description(self, line):
248 self._set_field('description', line, True)
249
250 def get_authname(self):
251 return self._get_field('authname')
252
253 def set_authname(self, name):
254 self._set_field('authname', name or git.author().name)
255
256 def get_authemail(self):
257 return self._get_field('authemail')
258
259 def set_authemail(self, email):
260 self._set_field('authemail', email or git.author().email)
261
262 def get_authdate(self):
263 return self._get_field('authdate')
264
265 def set_authdate(self, date):
266 self._set_field('authdate', date or git.author().date)
267
268 def get_commname(self):
269 return self._get_field('commname')
270
271 def set_commname(self, name):
272 self._set_field('commname', name or git.committer().name)
273
274 def get_commemail(self):
275 return self._get_field('commemail')
276
277 def set_commemail(self, email):
278 self._set_field('commemail', email or git.committer().email)
279
280 def get_log(self):
281 return self._get_field('log')
282
283 def set_log(self, value, backup = False):
284 self._set_field('log', value)
285 self.__update_log_ref(value)
286
287
288 class Series(StgitObject):
289 """Class including the operations on series
290 """
291 def __init__(self, name = None):
292 """Takes a series name as the parameter.
293 """
294 try:
295 if name:
296 self.__name = name
297 else:
298 self.__name = git.get_head_file()
299 self.__base_dir = basedir.get()
300 except git.GitException, ex:
301 raise StackException, 'GIT tree not initialised: %s' % ex
302
303 self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
304 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
305 self.__name)
306 self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
307 self.__name)
308
309 self.__applied_file = os.path.join(self._dir(), 'applied')
310 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
311 self.__hidden_file = os.path.join(self._dir(), 'hidden')
312 self.__current_file = os.path.join(self._dir(), 'current')
313 self.__descr_file = os.path.join(self._dir(), 'description')
314
315 # where this series keeps its patches
316 self.__patch_dir = os.path.join(self._dir(), 'patches')
317 if not os.path.isdir(self.__patch_dir):
318 self.__patch_dir = self._dir()
319
320 # if no __refs_dir, create and populate it (upgrade old repositories)
321 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
322 os.makedirs(self.__refs_dir)
323 for patch in self.get_applied() + self.get_unapplied():
324 self.get_patch(patch).update_top_ref()
325
326 # trash directory
327 self.__trash_dir = os.path.join(self._dir(), 'trash')
328 if self.is_initialised() and not os.path.isdir(self.__trash_dir):
329 os.makedirs(self.__trash_dir)
330
331 def __patch_name_valid(self, name):
332 """Raise an exception if the patch name is not valid.
333 """
334 if not name or re.search('[^\w.-]', name):
335 raise StackException, 'Invalid patch name: "%s"' % name
336
337 def get_branch(self):
338 """Return the branch name for the Series object
339 """
340 return self.__name
341
342 def __set_current(self, name):
343 """Sets the topmost patch
344 """
345 self._set_field('current', name)
346
347 def get_patch(self, name):
348 """Return a Patch object for the given name
349 """
350 return Patch(name, self.__patch_dir, self.__refs_dir)
351
352 def get_current_patch(self):
353 """Return a Patch object representing the topmost patch, or
354 None if there is no such patch."""
355 crt = self.get_current()
356 if not crt:
357 return None
358 return Patch(crt, self.__patch_dir, self.__refs_dir)
359
360 def get_current(self):
361 """Return the name of the topmost patch, or None if there is
362 no such patch."""
363 name = self._get_field('current')
364 if name == '':
365 return None
366 else:
367 return name
368
369 def get_applied(self):
370 if not os.path.isfile(self.__applied_file):
371 raise StackException, 'Branch "%s" not initialised' % self.__name
372 f = file(self.__applied_file)
373 names = [line.strip() for line in f.readlines()]
374 f.close()
375 return names
376
377 def get_unapplied(self):
378 if not os.path.isfile(self.__unapplied_file):
379 raise StackException, 'Branch "%s" not initialised' % self.__name
380 f = file(self.__unapplied_file)
381 names = [line.strip() for line in f.readlines()]
382 f.close()
383 return names
384
385 def get_hidden(self):
386 if not os.path.isfile(self.__hidden_file):
387 return []
388 f = file(self.__hidden_file)
389 names = [line.strip() for line in f.readlines()]
390 f.close()
391 return names
392
393 def get_base_file(self):
394 self.__begin_stack_check()
395 return self.__base_file
396
397 def get_protected(self):
398 return os.path.isfile(os.path.join(self._dir(), 'protected'))
399
400 def protect(self):
401 protect_file = os.path.join(self._dir(), 'protected')
402 if not os.path.isfile(protect_file):
403 create_empty_file(protect_file)
404
405 def unprotect(self):
406 protect_file = os.path.join(self._dir(), 'protected')
407 if os.path.isfile(protect_file):
408 os.remove(protect_file)
409
410 def get_description(self):
411 return self._get_field('description') or ''
412
413 def set_description(self, line):
414 self._set_field('description', line)
415
416 def get_parent_remote(self):
417 value = config.get('branch.%s.remote' % self.__name)
418 if value:
419 return value
420 elif 'origin' in git.remotes_list():
421 print 'Notice: no parent remote declared for stack "%s", defaulting to "origin".' \
422 'Consider setting "branch.%s.remote" with "git repo-config".' \
423 % (self.__name, self.__name)
424 return 'origin'
425 else:
426 raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
427
428 def __set_parent_remote(self, remote):
429 value = config.set('branch.%s.remote' % self.__name, remote)
430
431 def get_parent_branch(self):
432 value = config.get('branch.%s.merge' % self.__name)
433 if value:
434 return value
435 elif git.rev_parse('heads/origin'):
436 print 'Notice: no parent branch declared for stack "%s", defaulting to "heads/origin".' \
437 'Consider setting "branch.%s.merge" with "git repo-config".' \
438 % (self.__name, self.__name)
439 return 'heads/origin'
440 else:
441 raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
442
443 def __set_parent_branch(self, name):
444 config.set('branch.%s.merge' % self.__name, name)
445
446 def set_parent(self, remote, localbranch):
447 if localbranch:
448 self.__set_parent_branch(localbranch)
449 if remote:
450 self.__set_parent_remote(remote)
451 elif remote:
452 raise StackException, 'Remote "%s" without a branch cannot be used as parent' % remote
453
454 def __patch_is_current(self, patch):
455 return patch.get_name() == self.get_current()
456
457 def patch_applied(self, name):
458 """Return true if the patch exists in the applied list
459 """
460 return name in self.get_applied()
461
462 def patch_unapplied(self, name):
463 """Return true if the patch exists in the unapplied list
464 """
465 return name in self.get_unapplied()
466
467 def patch_hidden(self, name):
468 """Return true if the patch is hidden.
469 """
470 return name in self.get_hidden()
471
472 def patch_exists(self, name):
473 """Return true if there is a patch with the given name, false
474 otherwise."""
475 return self.patch_applied(name) or self.patch_unapplied(name)
476
477 def __begin_stack_check(self):
478 """Save the current HEAD into .git/refs/heads/base if the stack
479 is empty
480 """
481 if len(self.get_applied()) == 0:
482 head = git.get_head()
483 write_string(self.__base_file, head)
484
485 def __end_stack_check(self):
486 """Remove .git/refs/heads/base if the stack is empty.
487 This warning should never happen
488 """
489 if len(self.get_applied()) == 0 \
490 and read_string(self.__base_file) != git.get_head():
491 print 'Warning: stack empty but the HEAD and base are different'
492
493 def head_top_equal(self):
494 """Return true if the head and the top are the same
495 """
496 crt = self.get_current_patch()
497 if not crt:
498 # we don't care, no patches applied
499 return True
500 return git.get_head() == crt.get_top()
501
502 def is_initialised(self):
503 """Checks if series is already initialised
504 """
505 return os.path.isdir(self.__patch_dir)
506
507 def init(self, create_at=False, parent_remote=None, parent_branch=None):
508 """Initialises the stgit series
509 """
510 bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
511
512 if os.path.exists(self.__patch_dir):
513 raise StackException, self.__patch_dir + ' already exists'
514 if os.path.exists(self.__refs_dir):
515 raise StackException, self.__refs_dir + ' already exists'
516 if os.path.exists(self.__base_file):
517 raise StackException, self.__base_file + ' already exists'
518
519 if (create_at!=False):
520 git.create_branch(self.__name, create_at)
521
522 os.makedirs(self.__patch_dir)
523
524 self.set_parent(parent_remote, parent_branch)
525
526 create_dirs(bases_dir)
527
528 self.create_empty_field('applied')
529 self.create_empty_field('unapplied')
530 self.create_empty_field('description')
531 os.makedirs(os.path.join(self._dir(), 'patches'))
532 os.makedirs(self.__refs_dir)
533 self.__begin_stack_check()
534
535 def convert(self):
536 """Either convert to use a separate patch directory, or
537 unconvert to place the patches in the same directory with
538 series control files
539 """
540 if self.__patch_dir == self._dir():
541 print 'Converting old-style to new-style...',
542 sys.stdout.flush()
543
544 self.__patch_dir = os.path.join(self._dir(), 'patches')
545 os.makedirs(self.__patch_dir)
546
547 for p in self.get_applied() + self.get_unapplied():
548 src = os.path.join(self._dir(), p)
549 dest = os.path.join(self.__patch_dir, p)
550 os.rename(src, dest)
551
552 print 'done'
553
554 else:
555 print 'Converting new-style to old-style...',
556 sys.stdout.flush()
557
558 for p in self.get_applied() + self.get_unapplied():
559 src = os.path.join(self.__patch_dir, p)
560 dest = os.path.join(self._dir(), p)
561 os.rename(src, dest)
562
563 if not os.listdir(self.__patch_dir):
564 os.rmdir(self.__patch_dir)
565 print 'done'
566 else:
567 print 'Patch directory %s is not empty.' % self.__name
568
569 self.__patch_dir = self._dir()
570
571 def rename(self, to_name):
572 """Renames a series
573 """
574 to_stack = Series(to_name)
575
576 if to_stack.is_initialised():
577 raise StackException, '"%s" already exists' % to_stack.get_branch()
578 if os.path.exists(to_stack.__base_file):
579 os.remove(to_stack.__base_file)
580
581 git.rename_branch(self.__name, to_name)
582
583 if os.path.isdir(self._dir()):
584 rename(os.path.join(self.__base_dir, 'patches'),
585 self.__name, to_stack.__name)
586 if os.path.exists(self.__base_file):
587 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
588 self.__name, to_stack.__name)
589 if os.path.exists(self.__refs_dir):
590 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
591 self.__name, to_stack.__name)
592
593 # Rename the config section
594 config.rename_section("branch.%s" % self.__name,
595 "branch.%s" % to_name)
596
597 self.__init__(to_name)
598
599 def clone(self, target_series):
600 """Clones a series
601 """
602 try:
603 # allow cloning of branches not under StGIT control
604 base = read_string(self.get_base_file())
605 except:
606 base = git.get_head()
607 Series(target_series).init(create_at = base)
608 new_series = Series(target_series)
609
610 # generate an artificial description file
611 new_series.set_description('clone of "%s"' % self.__name)
612
613 # clone self's entire series as unapplied patches
614 try:
615 # allow cloning of branches not under StGIT control
616 applied = self.get_applied()
617 unapplied = self.get_unapplied()
618 patches = applied + unapplied
619 patches.reverse()
620 except:
621 patches = applied = unapplied = []
622 for p in patches:
623 patch = self.get_patch(p)
624 new_series.new_patch(p, message = patch.get_description(),
625 can_edit = False, unapplied = True,
626 bottom = patch.get_bottom(),
627 top = patch.get_top(),
628 author_name = patch.get_authname(),
629 author_email = patch.get_authemail(),
630 author_date = patch.get_authdate())
631
632 # fast forward the cloned series to self's top
633 new_series.forward_patches(applied)
634
635 # Clone remote and merge settings
636 value = config.get('branch.%s.remote' % self.__name)
637 if value:
638 config.set('branch.%s.remote' % target_series, value)
639
640 value = config.get('branch.%s.merge' % self.__name)
641 if value:
642 config.set('branch.%s.merge' % target_series, value)
643
644 def delete(self, force = False):
645 """Deletes an stgit series
646 """
647 if self.is_initialised():
648 patches = self.get_unapplied() + self.get_applied()
649 if not force and patches:
650 raise StackException, \
651 'Cannot delete: the series still contains patches'
652 for p in patches:
653 Patch(p, self.__patch_dir, self.__refs_dir).delete()
654
655 # remove the trash directory
656 for fname in os.listdir(self.__trash_dir):
657 os.remove(fname)
658 os.rmdir(self.__trash_dir)
659
660 # FIXME: find a way to get rid of those manual removals
661 # (move functionality to StgitObject ?)
662 if os.path.exists(self.__applied_file):
663 os.remove(self.__applied_file)
664 if os.path.exists(self.__unapplied_file):
665 os.remove(self.__unapplied_file)
666 if os.path.exists(self.__hidden_file):
667 os.remove(self.__hidden_file)
668 if os.path.exists(self.__current_file):
669 os.remove(self.__current_file)
670 if os.path.exists(self.__descr_file):
671 os.remove(self.__descr_file)
672 if not os.listdir(self.__patch_dir):
673 os.rmdir(self.__patch_dir)
674 else:
675 print 'Patch directory %s is not empty.' % self.__name
676 if not os.listdir(self._dir()):
677 remove_dirs(os.path.join(self.__base_dir, 'patches'),
678 self.__name)
679 else:
680 print 'Series directory %s is not empty.' % self.__name
681 if not os.listdir(self.__refs_dir):
682 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
683 self.__name)
684 else:
685 print 'Refs directory %s is not empty.' % self.__refs_dir
686
687 if os.path.exists(self.__base_file):
688 remove_file_and_dirs(
689 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
690
691 def refresh_patch(self, files = None, message = None, edit = False,
692 show_patch = False,
693 cache_update = True,
694 author_name = None, author_email = None,
695 author_date = None,
696 committer_name = None, committer_email = None,
697 backup = False, sign_str = None, log = 'refresh'):
698 """Generates a new commit for the given patch
699 """
700 name = self.get_current()
701 if not name:
702 raise StackException, 'No patches applied'
703
704 patch = Patch(name, self.__patch_dir, self.__refs_dir)
705
706 descr = patch.get_description()
707 if not (message or descr):
708 edit = True
709 descr = ''
710 elif message:
711 descr = message
712
713 if not message and edit:
714 descr = edit_file(self, descr.rstrip(), \
715 'Please edit the description for patch "%s" ' \
716 'above.' % name, show_patch)
717
718 if not author_name:
719 author_name = patch.get_authname()
720 if not author_email:
721 author_email = patch.get_authemail()
722 if not author_date:
723 author_date = patch.get_authdate()
724 if not committer_name:
725 committer_name = patch.get_commname()
726 if not committer_email:
727 committer_email = patch.get_commemail()
728
729 if sign_str:
730 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
731 committer_name, committer_email)
732
733 bottom = patch.get_bottom()
734
735 commit_id = git.commit(files = files,
736 message = descr, parents = [bottom],
737 cache_update = cache_update,
738 allowempty = True,
739 author_name = author_name,
740 author_email = author_email,
741 author_date = author_date,
742 committer_name = committer_name,
743 committer_email = committer_email)
744
745 patch.set_bottom(bottom, backup = backup)
746 patch.set_top(commit_id, backup = backup)
747 patch.set_description(descr)
748 patch.set_authname(author_name)
749 patch.set_authemail(author_email)
750 patch.set_authdate(author_date)
751 patch.set_commname(committer_name)
752 patch.set_commemail(committer_email)
753
754 if log:
755 self.log_patch(patch, log)
756
757 return commit_id
758
759 def undo_refresh(self):
760 """Undo the patch boundaries changes caused by 'refresh'
761 """
762 name = self.get_current()
763 assert(name)
764
765 patch = Patch(name, self.__patch_dir, self.__refs_dir)
766 old_bottom = patch.get_old_bottom()
767 old_top = patch.get_old_top()
768
769 # the bottom of the patch is not changed by refresh. If the
770 # old_bottom is different, there wasn't any previous 'refresh'
771 # command (probably only a 'push')
772 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
773 raise StackException, 'No undo information available'
774
775 git.reset(tree_id = old_top, check_out = False)
776 if patch.restore_old_boundaries():
777 self.log_patch(patch, 'undo')
778
779 def new_patch(self, name, message = None, can_edit = True,
780 unapplied = False, show_patch = False,
781 top = None, bottom = None,
782 author_name = None, author_email = None, author_date = None,
783 committer_name = None, committer_email = None,
784 before_existing = False, refresh = True):
785 """Creates a new patch
786 """
787 self.__patch_name_valid(name)
788
789 if self.patch_applied(name) or self.patch_unapplied(name):
790 raise StackException, 'Patch "%s" already exists' % name
791
792 if not message and can_edit:
793 descr = edit_file(self, None, \
794 'Please enter the description for patch "%s" ' \
795 'above.' % name, show_patch)
796 else:
797 descr = message
798
799 head = git.get_head()
800
801 self.__begin_stack_check()
802
803 patch = Patch(name, self.__patch_dir, self.__refs_dir)
804 patch.create()
805
806 if bottom:
807 patch.set_bottom(bottom)
808 else:
809 patch.set_bottom(head)
810 if top:
811 patch.set_top(top)
812 else:
813 patch.set_top(head)
814
815 patch.set_description(descr)
816 patch.set_authname(author_name)
817 patch.set_authemail(author_email)
818 patch.set_authdate(author_date)
819 patch.set_commname(committer_name)
820 patch.set_commemail(committer_email)
821
822 if unapplied:
823 self.log_patch(patch, 'new')
824
825 patches = [patch.get_name()] + self.get_unapplied()
826
827 f = file(self.__unapplied_file, 'w+')
828 f.writelines([line + '\n' for line in patches])
829 f.close()
830 elif before_existing:
831 self.log_patch(patch, 'new')
832
833 insert_string(self.__applied_file, patch.get_name())
834 if not self.get_current():
835 self.__set_current(name)
836 else:
837 append_string(self.__applied_file, patch.get_name())
838 self.__set_current(name)
839 if refresh:
840 self.refresh_patch(cache_update = False, log = 'new')
841
842 def delete_patch(self, name):
843 """Deletes a patch
844 """
845 self.__patch_name_valid(name)
846 patch = Patch(name, self.__patch_dir, self.__refs_dir)
847
848 if self.__patch_is_current(patch):
849 self.pop_patch(name)
850 elif self.patch_applied(name):
851 raise StackException, 'Cannot remove an applied patch, "%s", ' \
852 'which is not current' % name
853 elif not name in self.get_unapplied():
854 raise StackException, 'Unknown patch "%s"' % name
855
856 # save the commit id to a trash file
857 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
858
859 patch.delete()
860
861 unapplied = self.get_unapplied()
862 unapplied.remove(name)
863 f = file(self.__unapplied_file, 'w+')
864 f.writelines([line + '\n' for line in unapplied])
865 f.close()
866
867 if self.patch_hidden(name):
868 self.unhide_patch(name)
869
870 self.__begin_stack_check()
871
872 def forward_patches(self, names):
873 """Try to fast-forward an array of patches.
874
875 On return, patches in names[0:returned_value] have been pushed on the
876 stack. Apply the rest with push_patch
877 """
878 unapplied = self.get_unapplied()
879 self.__begin_stack_check()
880
881 forwarded = 0
882 top = git.get_head()
883
884 for name in names:
885 assert(name in unapplied)
886
887 patch = Patch(name, self.__patch_dir, self.__refs_dir)
888
889 head = top
890 bottom = patch.get_bottom()
891 top = patch.get_top()
892
893 # top != bottom always since we have a commit for each patch
894 if head == bottom:
895 # reset the backup information. No logging since the
896 # patch hasn't changed
897 patch.set_bottom(head, backup = True)
898 patch.set_top(top, backup = True)
899
900 else:
901 head_tree = git.get_commit(head).get_tree()
902 bottom_tree = git.get_commit(bottom).get_tree()
903 if head_tree == bottom_tree:
904 # We must just reparent this patch and create a new commit
905 # for it
906 descr = patch.get_description()
907 author_name = patch.get_authname()
908 author_email = patch.get_authemail()
909 author_date = patch.get_authdate()
910 committer_name = patch.get_commname()
911 committer_email = patch.get_commemail()
912
913 top_tree = git.get_commit(top).get_tree()
914
915 top = git.commit(message = descr, parents = [head],
916 cache_update = False,
917 tree_id = top_tree,
918 allowempty = True,
919 author_name = author_name,
920 author_email = author_email,
921 author_date = author_date,
922 committer_name = committer_name,
923 committer_email = committer_email)
924
925 patch.set_bottom(head, backup = True)
926 patch.set_top(top, backup = True)
927
928 self.log_patch(patch, 'push(f)')
929 else:
930 top = head
931 # stop the fast-forwarding, must do a real merge
932 break
933
934 forwarded+=1
935 unapplied.remove(name)
936
937 if forwarded == 0:
938 return 0
939
940 git.switch(top)
941
942 append_strings(self.__applied_file, names[0:forwarded])
943
944 f = file(self.__unapplied_file, 'w+')
945 f.writelines([line + '\n' for line in unapplied])
946 f.close()
947
948 self.__set_current(name)
949
950 return forwarded
951
952 def merged_patches(self, names):
953 """Test which patches were merged upstream by reverse-applying
954 them in reverse order. The function returns the list of
955 patches detected to have been applied. The state of the tree
956 is restored to the original one
957 """
958 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
959 for name in names]
960 patches.reverse()
961
962 merged = []
963 for p in patches:
964 if git.apply_diff(p.get_top(), p.get_bottom()):
965 merged.append(p.get_name())
966 merged.reverse()
967
968 git.reset()
969
970 return merged
971
972 def push_patch(self, name, empty = False):
973 """Pushes a patch on the stack
974 """
975 unapplied = self.get_unapplied()
976 assert(name in unapplied)
977
978 self.__begin_stack_check()
979
980 patch = Patch(name, self.__patch_dir, self.__refs_dir)
981
982 head = git.get_head()
983 bottom = patch.get_bottom()
984 top = patch.get_top()
985
986 ex = None
987 modified = False
988
989 # top != bottom always since we have a commit for each patch
990 if empty:
991 # just make an empty patch (top = bottom = HEAD). This
992 # option is useful to allow undoing already merged
993 # patches. The top is updated by refresh_patch since we
994 # need an empty commit
995 patch.set_bottom(head, backup = True)
996 patch.set_top(head, backup = True)
997 modified = True
998 elif head == bottom:
999 # reset the backup information. No need for logging
1000 patch.set_bottom(bottom, backup = True)
1001 patch.set_top(top, backup = True)
1002
1003 git.switch(top)
1004 else:
1005 # new patch needs to be refreshed.
1006 # The current patch is empty after merge.
1007 patch.set_bottom(head, backup = True)
1008 patch.set_top(head, backup = True)
1009
1010 # Try the fast applying first. If this fails, fall back to the
1011 # three-way merge
1012 if not git.apply_diff(bottom, top):
1013 # if git.apply_diff() fails, the patch requires a diff3
1014 # merge and can be reported as modified
1015 modified = True
1016
1017 # merge can fail but the patch needs to be pushed
1018 try:
1019 git.merge(bottom, head, top, recursive = True)
1020 except git.GitException, ex:
1021 print >> sys.stderr, \
1022 'The merge failed during "push". ' \
1023 'Use "refresh" after fixing the conflicts'
1024
1025 append_string(self.__applied_file, name)
1026
1027 unapplied.remove(name)
1028 f = file(self.__unapplied_file, 'w+')
1029 f.writelines([line + '\n' for line in unapplied])
1030 f.close()
1031
1032 self.__set_current(name)
1033
1034 # head == bottom case doesn't need to refresh the patch
1035 if empty or head != bottom:
1036 if not ex:
1037 # if the merge was OK and no conflicts, just refresh the patch
1038 # The GIT cache was already updated by the merge operation
1039 if modified:
1040 log = 'push(m)'
1041 else:
1042 log = 'push'
1043 self.refresh_patch(cache_update = False, log = log)
1044 else:
1045 # we store the correctly merged files only for
1046 # tracking the conflict history. Note that the
1047 # git.merge() operations should always leave the index
1048 # in a valid state (i.e. only stage 0 files)
1049 self.refresh_patch(cache_update = False, log = 'push(c)')
1050 raise StackException, str(ex)
1051
1052 return modified
1053
1054 def undo_push(self):
1055 name = self.get_current()
1056 assert(name)
1057
1058 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1059 old_bottom = patch.get_old_bottom()
1060 old_top = patch.get_old_top()
1061
1062 # the top of the patch is changed by a push operation only
1063 # together with the bottom (otherwise the top was probably
1064 # modified by 'refresh'). If they are both unchanged, there
1065 # was a fast forward
1066 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1067 raise StackException, 'No undo information available'
1068
1069 git.reset()
1070 self.pop_patch(name)
1071 ret = patch.restore_old_boundaries()
1072 if ret:
1073 self.log_patch(patch, 'undo')
1074
1075 return ret
1076
1077 def pop_patch(self, name, keep = False):
1078 """Pops the top patch from the stack
1079 """
1080 applied = self.get_applied()
1081 applied.reverse()
1082 assert(name in applied)
1083
1084 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1085
1086 # only keep the local changes
1087 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1088 raise StackException, \
1089 'Failed to pop patches while preserving the local changes'
1090
1091 git.switch(patch.get_bottom(), keep)
1092
1093 # save the new applied list
1094 idx = applied.index(name) + 1
1095
1096 popped = applied[:idx]
1097 popped.reverse()
1098 unapplied = popped + self.get_unapplied()
1099
1100 f = file(self.__unapplied_file, 'w+')
1101 f.writelines([line + '\n' for line in unapplied])
1102 f.close()
1103
1104 del applied[:idx]
1105 applied.reverse()
1106
1107 f = file(self.__applied_file, 'w+')
1108 f.writelines([line + '\n' for line in applied])
1109 f.close()
1110
1111 if applied == []:
1112 self.__set_current(None)
1113 else:
1114 self.__set_current(applied[-1])
1115
1116 self.__end_stack_check()
1117
1118 def empty_patch(self, name):
1119 """Returns True if the patch is empty
1120 """
1121 self.__patch_name_valid(name)
1122 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1123 bottom = patch.get_bottom()
1124 top = patch.get_top()
1125
1126 if bottom == top:
1127 return True
1128 elif git.get_commit(top).get_tree() \
1129 == git.get_commit(bottom).get_tree():
1130 return True
1131
1132 return False
1133
1134 def rename_patch(self, oldname, newname):
1135 self.__patch_name_valid(newname)
1136
1137 applied = self.get_applied()
1138 unapplied = self.get_unapplied()
1139
1140 if oldname == newname:
1141 raise StackException, '"To" name and "from" name are the same'
1142
1143 if newname in applied or newname in unapplied:
1144 raise StackException, 'Patch "%s" already exists' % newname
1145
1146 if self.patch_hidden(oldname):
1147 self.unhide_patch(oldname)
1148 self.hide_patch(newname)
1149
1150 if oldname in unapplied:
1151 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1152 unapplied[unapplied.index(oldname)] = newname
1153
1154 f = file(self.__unapplied_file, 'w+')
1155 f.writelines([line + '\n' for line in unapplied])
1156 f.close()
1157 elif oldname in applied:
1158 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1159 if oldname == self.get_current():
1160 self.__set_current(newname)
1161
1162 applied[applied.index(oldname)] = newname
1163
1164 f = file(self.__applied_file, 'w+')
1165 f.writelines([line + '\n' for line in applied])
1166 f.close()
1167 else:
1168 raise StackException, 'Unknown patch "%s"' % oldname
1169
1170 def log_patch(self, patch, message):
1171 """Generate a log commit for a patch
1172 """
1173 top = git.get_commit(patch.get_top())
1174 msg = '%s\t%s' % (message, top.get_id_hash())
1175
1176 old_log = patch.get_log()
1177 if old_log:
1178 parents = [old_log]
1179 else:
1180 parents = []
1181
1182 log = git.commit(message = msg, parents = parents,
1183 cache_update = False, tree_id = top.get_tree(),
1184 allowempty = True)
1185 patch.set_log(log)
1186
1187 def hide_patch(self, name):
1188 """Add the patch to the hidden list.
1189 """
1190 if not self.patch_exists(name):
1191 raise StackException, 'Unknown patch "%s"' % name
1192 elif self.patch_hidden(name):
1193 raise StackException, 'Patch "%s" already hidden' % name
1194
1195 append_string(self.__hidden_file, name)
1196
1197 def unhide_patch(self, name):
1198 """Add the patch to the hidden list.
1199 """
1200 if not self.patch_exists(name):
1201 raise StackException, 'Unknown patch "%s"' % name
1202 hidden = self.get_hidden()
1203 if not name in hidden:
1204 raise StackException, 'Patch "%s" not hidden' % name
1205
1206 hidden.remove(name)
1207
1208 f = file(self.__hidden_file, 'w+')
1209 f.writelines([line + '\n' for line in hidden])
1210 f.close()