Make Series::patch_applied public.
[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
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 if config.has_option('stgit', 'editor'):
96 editor = config.get('stgit', 'editor')
97 elif 'EDITOR' in os.environ:
98 editor = os.environ['EDITOR']
99 else:
100 editor = 'vi'
101 editor += ' %s' % fname
102
103 print 'Invoking the editor: "%s"...' % editor,
104 sys.stdout.flush()
105 print 'done (exit code: %d)' % os.system(editor)
106
107 f = file(fname, 'r+')
108
109 __clean_comments(f)
110 f.seek(0)
111 result = f.read()
112
113 f.close()
114 os.remove(fname)
115
116 return result
117
118 #
119 # Classes
120 #
121
122 class Patch:
123 """Basic patch implementation
124 """
125 def __init__(self, name, series_dir, refs_dir):
126 self.__series_dir = series_dir
127 self.__name = name
128 self.__dir = os.path.join(self.__series_dir, self.__name)
129 self.__refs_dir = refs_dir
130 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
131 self.__log_ref_file = os.path.join(self.__refs_dir,
132 self.__name + '.log')
133
134 def create(self):
135 os.mkdir(self.__dir)
136 create_empty_file(os.path.join(self.__dir, 'bottom'))
137 create_empty_file(os.path.join(self.__dir, 'top'))
138
139 def delete(self):
140 for f in os.listdir(self.__dir):
141 os.remove(os.path.join(self.__dir, f))
142 os.rmdir(self.__dir)
143 os.remove(self.__top_ref_file)
144 if os.path.exists(self.__log_ref_file):
145 os.remove(self.__log_ref_file)
146
147 def get_name(self):
148 return self.__name
149
150 def rename(self, newname):
151 olddir = self.__dir
152 old_top_ref_file = self.__top_ref_file
153 old_log_ref_file = self.__log_ref_file
154 self.__name = newname
155 self.__dir = os.path.join(self.__series_dir, self.__name)
156 self.__top_ref_file = os.path.join(self.__refs_dir, self.__name)
157 self.__log_ref_file = os.path.join(self.__refs_dir,
158 self.__name + '.log')
159
160 os.rename(olddir, self.__dir)
161 os.rename(old_top_ref_file, self.__top_ref_file)
162 if os.path.exists(old_log_ref_file):
163 os.rename(old_log_ref_file, self.__log_ref_file)
164
165 def __update_top_ref(self, ref):
166 write_string(self.__top_ref_file, ref)
167
168 def __update_log_ref(self, ref):
169 write_string(self.__log_ref_file, ref)
170
171 def update_top_ref(self):
172 top = self.get_top()
173 if top:
174 self.__update_top_ref(top)
175
176 def __get_field(self, name, multiline = False):
177 id_file = os.path.join(self.__dir, name)
178 if os.path.isfile(id_file):
179 line = read_string(id_file, multiline)
180 if line == '':
181 return None
182 else:
183 return line
184 else:
185 return None
186
187 def __set_field(self, name, value, multiline = False):
188 fname = os.path.join(self.__dir, name)
189 if value and value != '':
190 write_string(fname, value, multiline)
191 elif os.path.isfile(fname):
192 os.remove(fname)
193
194 def get_old_bottom(self):
195 return self.__get_field('bottom.old')
196
197 def get_bottom(self):
198 return self.__get_field('bottom')
199
200 def set_bottom(self, value, backup = False):
201 if backup:
202 curr = self.__get_field('bottom')
203 self.__set_field('bottom.old', curr)
204 self.__set_field('bottom', value)
205
206 def get_old_top(self):
207 return self.__get_field('top.old')
208
209 def get_top(self):
210 return self.__get_field('top')
211
212 def set_top(self, value, backup = False):
213 if backup:
214 curr = self.__get_field('top')
215 self.__set_field('top.old', curr)
216 self.__set_field('top', value)
217 self.__update_top_ref(value)
218
219 def restore_old_boundaries(self):
220 bottom = self.__get_field('bottom.old')
221 top = self.__get_field('top.old')
222
223 if top and bottom:
224 self.__set_field('bottom', bottom)
225 self.__set_field('top', top)
226 self.__update_top_ref(top)
227 return True
228 else:
229 return False
230
231 def get_description(self):
232 return self.__get_field('description', True)
233
234 def set_description(self, line):
235 self.__set_field('description', line, True)
236
237 def get_authname(self):
238 return self.__get_field('authname')
239
240 def set_authname(self, name):
241 if not name:
242 if config.has_option('stgit', 'authname'):
243 name = config.get('stgit', 'authname')
244 elif 'GIT_AUTHOR_NAME' in os.environ:
245 name = os.environ['GIT_AUTHOR_NAME']
246 self.__set_field('authname', name)
247
248 def get_authemail(self):
249 return self.__get_field('authemail')
250
251 def set_authemail(self, address):
252 if not address:
253 if config.has_option('stgit', 'authemail'):
254 address = config.get('stgit', 'authemail')
255 elif 'GIT_AUTHOR_EMAIL' in os.environ:
256 address = os.environ['GIT_AUTHOR_EMAIL']
257 self.__set_field('authemail', address)
258
259 def get_authdate(self):
260 return self.__get_field('authdate')
261
262 def set_authdate(self, date):
263 if not date and 'GIT_AUTHOR_DATE' in os.environ:
264 date = os.environ['GIT_AUTHOR_DATE']
265 self.__set_field('authdate', date)
266
267 def get_commname(self):
268 return self.__get_field('commname')
269
270 def set_commname(self, name):
271 if not name:
272 if config.has_option('stgit', 'commname'):
273 name = config.get('stgit', 'commname')
274 elif 'GIT_COMMITTER_NAME' in os.environ:
275 name = os.environ['GIT_COMMITTER_NAME']
276 self.__set_field('commname', name)
277
278 def get_commemail(self):
279 return self.__get_field('commemail')
280
281 def set_commemail(self, address):
282 if not address:
283 if config.has_option('stgit', 'commemail'):
284 address = config.get('stgit', 'commemail')
285 elif 'GIT_COMMITTER_EMAIL' in os.environ:
286 address = os.environ['GIT_COMMITTER_EMAIL']
287 self.__set_field('commemail', address)
288
289 def get_log(self):
290 return self.__get_field('log')
291
292 def set_log(self, value, backup = False):
293 self.__set_field('log', value)
294 self.__update_log_ref(value)
295
296
297 class Series:
298 """Class including the operations on series
299 """
300 def __init__(self, name = None):
301 """Takes a series name as the parameter.
302 """
303 try:
304 if name:
305 self.__name = name
306 else:
307 self.__name = git.get_head_file()
308 self.__base_dir = basedir.get()
309 except git.GitException, ex:
310 raise StackException, 'GIT tree not initialised: %s' % ex
311
312 self.__series_dir = os.path.join(self.__base_dir, 'patches',
313 self.__name)
314 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
315 self.__name)
316 self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases',
317 self.__name)
318
319 self.__applied_file = os.path.join(self.__series_dir, 'applied')
320 self.__unapplied_file = os.path.join(self.__series_dir, 'unapplied')
321 self.__current_file = os.path.join(self.__series_dir, 'current')
322 self.__descr_file = os.path.join(self.__series_dir, 'description')
323
324 # where this series keeps its patches
325 self.__patch_dir = os.path.join(self.__series_dir, 'patches')
326 if not os.path.isdir(self.__patch_dir):
327 self.__patch_dir = self.__series_dir
328
329 # if no __refs_dir, create and populate it (upgrade old repositories)
330 if self.is_initialised() and not os.path.isdir(self.__refs_dir):
331 os.makedirs(self.__refs_dir)
332 for patch in self.get_applied() + self.get_unapplied():
333 self.get_patch(patch).update_top_ref()
334
335 # trash directory
336 self.__trash_dir = os.path.join(self.__series_dir, 'trash')
337 if self.is_initialised() and not os.path.isdir(self.__trash_dir):
338 os.makedirs(self.__trash_dir)
339
340 def get_branch(self):
341 """Return the branch name for the Series object
342 """
343 return self.__name
344
345 def __set_current(self, name):
346 """Sets the topmost patch
347 """
348 if name:
349 write_string(self.__current_file, name)
350 else:
351 create_empty_file(self.__current_file)
352
353 def get_patch(self, name):
354 """Return a Patch object for the given name
355 """
356 return Patch(name, self.__patch_dir, self.__refs_dir)
357
358 def get_current_patch(self):
359 """Return a Patch object representing the topmost patch, or
360 None if there is no such patch."""
361 crt = self.get_current()
362 if not crt:
363 return None
364 return Patch(crt, self.__patch_dir, self.__refs_dir)
365
366 def get_current(self):
367 """Return the name of the topmost patch, or None if there is
368 no such patch."""
369 if os.path.isfile(self.__current_file):
370 name = read_string(self.__current_file)
371 else:
372 return None
373 if name == '':
374 return None
375 else:
376 return name
377
378 def get_applied(self):
379 if not os.path.isfile(self.__applied_file):
380 raise StackException, 'Branch "%s" not initialised' % self.__name
381 f = file(self.__applied_file)
382 names = [line.strip() for line in f.readlines()]
383 f.close()
384 return names
385
386 def get_unapplied(self):
387 if not os.path.isfile(self.__unapplied_file):
388 raise StackException, 'Branch "%s" not initialised' % self.__name
389 f = file(self.__unapplied_file)
390 names = [line.strip() for line in f.readlines()]
391 f.close()
392 return names
393
394 def get_base_file(self):
395 self.__begin_stack_check()
396 return self.__base_file
397
398 def get_protected(self):
399 return os.path.isfile(os.path.join(self.__series_dir, 'protected'))
400
401 def protect(self):
402 protect_file = os.path.join(self.__series_dir, 'protected')
403 if not os.path.isfile(protect_file):
404 create_empty_file(protect_file)
405
406 def unprotect(self):
407 protect_file = os.path.join(self.__series_dir, 'protected')
408 if os.path.isfile(protect_file):
409 os.remove(protect_file)
410
411 def get_description(self):
412 if os.path.isfile(self.__descr_file):
413 return read_string(self.__descr_file)
414 else:
415 return ''
416
417 def __patch_is_current(self, patch):
418 return patch.get_name() == read_string(self.__current_file)
419
420 def patch_applied(self, name):
421 """Return true if the patch exists in the applied list
422 """
423 return name in self.get_applied()
424
425 def patch_unapplied(self, name):
426 """Return true if the patch exists in the unapplied list
427 """
428 return name in self.get_unapplied()
429
430 def patch_exists(self, name):
431 """Return true if there is a patch with the given name, false
432 otherwise."""
433 return self.patch_applied(name) or self.patch_unapplied(name)
434
435 def __begin_stack_check(self):
436 """Save the current HEAD into .git/refs/heads/base if the stack
437 is empty
438 """
439 if len(self.get_applied()) == 0:
440 head = git.get_head()
441 write_string(self.__base_file, head)
442
443 def __end_stack_check(self):
444 """Remove .git/refs/heads/base if the stack is empty.
445 This warning should never happen
446 """
447 if len(self.get_applied()) == 0 \
448 and read_string(self.__base_file) != git.get_head():
449 print 'Warning: stack empty but the HEAD and base are different'
450
451 def head_top_equal(self):
452 """Return true if the head and the top are the same
453 """
454 crt = self.get_current_patch()
455 if not crt:
456 # we don't care, no patches applied
457 return True
458 return git.get_head() == crt.get_top()
459
460 def is_initialised(self):
461 """Checks if series is already initialised
462 """
463 return os.path.isdir(self.__patch_dir)
464
465 def init(self, create_at=False):
466 """Initialises the stgit series
467 """
468 bases_dir = os.path.join(self.__base_dir, 'refs', 'bases')
469
470 if os.path.exists(self.__patch_dir):
471 raise StackException, self.__patch_dir + ' already exists'
472 if os.path.exists(self.__refs_dir):
473 raise StackException, self.__refs_dir + ' already exists'
474 if os.path.exists(self.__base_file):
475 raise StackException, self.__base_file + ' already exists'
476
477 if (create_at!=False):
478 git.create_branch(self.__name, create_at)
479
480 os.makedirs(self.__patch_dir)
481
482 create_dirs(bases_dir)
483
484 create_empty_file(self.__applied_file)
485 create_empty_file(self.__unapplied_file)
486 create_empty_file(self.__descr_file)
487 os.makedirs(os.path.join(self.__series_dir, 'patches'))
488 os.makedirs(self.__refs_dir)
489 self.__begin_stack_check()
490
491 def convert(self):
492 """Either convert to use a separate patch directory, or
493 unconvert to place the patches in the same directory with
494 series control files
495 """
496 if self.__patch_dir == self.__series_dir:
497 print 'Converting old-style to new-style...',
498 sys.stdout.flush()
499
500 self.__patch_dir = os.path.join(self.__series_dir, 'patches')
501 os.makedirs(self.__patch_dir)
502
503 for p in self.get_applied() + self.get_unapplied():
504 src = os.path.join(self.__series_dir, p)
505 dest = os.path.join(self.__patch_dir, p)
506 os.rename(src, dest)
507
508 print 'done'
509
510 else:
511 print 'Converting new-style to old-style...',
512 sys.stdout.flush()
513
514 for p in self.get_applied() + self.get_unapplied():
515 src = os.path.join(self.__patch_dir, p)
516 dest = os.path.join(self.__series_dir, p)
517 os.rename(src, dest)
518
519 if not os.listdir(self.__patch_dir):
520 os.rmdir(self.__patch_dir)
521 print 'done'
522 else:
523 print 'Patch directory %s is not empty.' % self.__name
524
525 self.__patch_dir = self.__series_dir
526
527 def rename(self, to_name):
528 """Renames a series
529 """
530 to_stack = Series(to_name)
531
532 if to_stack.is_initialised():
533 raise StackException, '"%s" already exists' % to_stack.get_branch()
534 if os.path.exists(to_stack.__base_file):
535 os.remove(to_stack.__base_file)
536
537 git.rename_branch(self.__name, to_name)
538
539 if os.path.isdir(self.__series_dir):
540 rename(os.path.join(self.__base_dir, 'patches'),
541 self.__name, to_stack.__name)
542 if os.path.exists(self.__base_file):
543 rename(os.path.join(self.__base_dir, 'refs', 'bases'),
544 self.__name, to_stack.__name)
545 if os.path.exists(self.__refs_dir):
546 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
547 self.__name, to_stack.__name)
548
549 self.__init__(to_name)
550
551 def clone(self, target_series):
552 """Clones a series
553 """
554 base = read_string(self.get_base_file())
555 Series(target_series).init(create_at = base)
556 new_series = Series(target_series)
557
558 # generate an artificial description file
559 write_string(new_series.__descr_file, 'clone of "%s"' % self.__name)
560
561 # clone self's entire series as unapplied patches
562 patches = self.get_applied() + self.get_unapplied()
563 patches.reverse()
564 for p in patches:
565 patch = self.get_patch(p)
566 new_series.new_patch(p, message = patch.get_description(),
567 can_edit = False, unapplied = True,
568 bottom = patch.get_bottom(),
569 top = patch.get_top(),
570 author_name = patch.get_authname(),
571 author_email = patch.get_authemail(),
572 author_date = patch.get_authdate())
573
574 # fast forward the cloned series to self's top
575 new_series.forward_patches(self.get_applied())
576
577 def delete(self, force = False):
578 """Deletes an stgit series
579 """
580 if self.is_initialised():
581 patches = self.get_unapplied() + self.get_applied()
582 if not force and patches:
583 raise StackException, \
584 'Cannot delete: the series still contains patches'
585 for p in patches:
586 Patch(p, self.__patch_dir, self.__refs_dir).delete()
587
588 # remove the trash directory
589 for fname in os.listdir(self.__trash_dir):
590 os.remove(fname)
591 os.rmdir(self.__trash_dir)
592
593 if os.path.exists(self.__applied_file):
594 os.remove(self.__applied_file)
595 if os.path.exists(self.__unapplied_file):
596 os.remove(self.__unapplied_file)
597 if os.path.exists(self.__current_file):
598 os.remove(self.__current_file)
599 if os.path.exists(self.__descr_file):
600 os.remove(self.__descr_file)
601 if not os.listdir(self.__patch_dir):
602 os.rmdir(self.__patch_dir)
603 else:
604 print 'Patch directory %s is not empty.' % self.__name
605 if not os.listdir(self.__series_dir):
606 remove_dirs(os.path.join(self.__base_dir, 'patches'),
607 self.__name)
608 else:
609 print 'Series directory %s is not empty.' % self.__name
610 if not os.listdir(self.__refs_dir):
611 remove_dirs(os.path.join(self.__base_dir, 'refs', 'patches'),
612 self.__name)
613 else:
614 print 'Refs directory %s is not empty.' % self.__refs_dir
615
616 if os.path.exists(self.__base_file):
617 remove_file_and_dirs(
618 os.path.join(self.__base_dir, 'refs', 'bases'), self.__name)
619
620 def refresh_patch(self, files = None, message = None, edit = False,
621 show_patch = False,
622 cache_update = True,
623 author_name = None, author_email = None,
624 author_date = None,
625 committer_name = None, committer_email = None,
626 backup = False, sign_str = None, log = 'refresh'):
627 """Generates a new commit for the given patch
628 """
629 name = self.get_current()
630 if not name:
631 raise StackException, 'No patches applied'
632
633 patch = Patch(name, self.__patch_dir, self.__refs_dir)
634
635 descr = patch.get_description()
636 if not (message or descr):
637 edit = True
638 descr = ''
639 elif message:
640 descr = message
641
642 if not message and edit:
643 descr = edit_file(self, descr.rstrip(), \
644 'Please edit the description for patch "%s" ' \
645 'above.' % name, show_patch)
646
647 if not author_name:
648 author_name = patch.get_authname()
649 if not author_email:
650 author_email = patch.get_authemail()
651 if not author_date:
652 author_date = patch.get_authdate()
653 if not committer_name:
654 committer_name = patch.get_commname()
655 if not committer_email:
656 committer_email = patch.get_commemail()
657
658 if sign_str:
659 descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str,
660 committer_name, committer_email)
661
662 bottom = patch.get_bottom()
663
664 commit_id = git.commit(files = files,
665 message = descr, parents = [bottom],
666 cache_update = cache_update,
667 allowempty = True,
668 author_name = author_name,
669 author_email = author_email,
670 author_date = author_date,
671 committer_name = committer_name,
672 committer_email = committer_email)
673
674 patch.set_bottom(bottom, backup = backup)
675 patch.set_top(commit_id, backup = backup)
676 patch.set_description(descr)
677 patch.set_authname(author_name)
678 patch.set_authemail(author_email)
679 patch.set_authdate(author_date)
680 patch.set_commname(committer_name)
681 patch.set_commemail(committer_email)
682
683 if log:
684 self.log_patch(patch, log)
685
686 return commit_id
687
688 def undo_refresh(self):
689 """Undo the patch boundaries changes caused by 'refresh'
690 """
691 name = self.get_current()
692 assert(name)
693
694 patch = Patch(name, self.__patch_dir, self.__refs_dir)
695 old_bottom = patch.get_old_bottom()
696 old_top = patch.get_old_top()
697
698 # the bottom of the patch is not changed by refresh. If the
699 # old_bottom is different, there wasn't any previous 'refresh'
700 # command (probably only a 'push')
701 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
702 raise StackException, 'No refresh undo information available'
703
704 git.reset(tree_id = old_top, check_out = False)
705 if patch.restore_old_boundaries():
706 self.log_patch(patch, 'undo')
707
708 def new_patch(self, name, message = None, can_edit = True,
709 unapplied = False, show_patch = False,
710 top = None, bottom = None,
711 author_name = None, author_email = None, author_date = None,
712 committer_name = None, committer_email = None,
713 before_existing = False, refresh = True):
714 """Creates a new patch
715 """
716 if self.patch_applied(name) or self.patch_unapplied(name):
717 raise StackException, 'Patch "%s" already exists' % name
718
719 if not message and can_edit:
720 descr = edit_file(self, None, \
721 'Please enter the description for patch "%s" ' \
722 'above.' % name, show_patch)
723 else:
724 descr = message
725
726 head = git.get_head()
727
728 self.__begin_stack_check()
729
730 patch = Patch(name, self.__patch_dir, self.__refs_dir)
731 patch.create()
732
733 if bottom:
734 patch.set_bottom(bottom)
735 else:
736 patch.set_bottom(head)
737 if top:
738 patch.set_top(top)
739 else:
740 patch.set_top(head)
741
742 patch.set_description(descr)
743 patch.set_authname(author_name)
744 patch.set_authemail(author_email)
745 patch.set_authdate(author_date)
746 patch.set_commname(committer_name)
747 patch.set_commemail(committer_email)
748
749 if unapplied:
750 self.log_patch(patch, 'new')
751
752 patches = [patch.get_name()] + self.get_unapplied()
753
754 f = file(self.__unapplied_file, 'w+')
755 f.writelines([line + '\n' for line in patches])
756 f.close()
757 elif before_existing:
758 self.log_patch(patch, 'new')
759
760 insert_string(self.__applied_file, patch.get_name())
761 if not self.get_current():
762 self.__set_current(name)
763 else:
764 append_string(self.__applied_file, patch.get_name())
765 self.__set_current(name)
766 if refresh:
767 self.refresh_patch(cache_update = False, log = 'new')
768
769 def delete_patch(self, name):
770 """Deletes a patch
771 """
772 patch = Patch(name, self.__patch_dir, self.__refs_dir)
773
774 if self.__patch_is_current(patch):
775 self.pop_patch(name)
776 elif self.patch_applied(name):
777 raise StackException, 'Cannot remove an applied patch, "%s", ' \
778 'which is not current' % name
779 elif not name in self.get_unapplied():
780 raise StackException, 'Unknown patch "%s"' % name
781
782 # save the commit id to a trash file
783 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
784
785 patch.delete()
786
787 unapplied = self.get_unapplied()
788 unapplied.remove(name)
789 f = file(self.__unapplied_file, 'w+')
790 f.writelines([line + '\n' for line in unapplied])
791 f.close()
792 self.__begin_stack_check()
793
794 def forward_patches(self, names):
795 """Try to fast-forward an array of patches.
796
797 On return, patches in names[0:returned_value] have been pushed on the
798 stack. Apply the rest with push_patch
799 """
800 unapplied = self.get_unapplied()
801 self.__begin_stack_check()
802
803 forwarded = 0
804 top = git.get_head()
805
806 for name in names:
807 assert(name in unapplied)
808
809 patch = Patch(name, self.__patch_dir, self.__refs_dir)
810
811 head = top
812 bottom = patch.get_bottom()
813 top = patch.get_top()
814
815 # top != bottom always since we have a commit for each patch
816 if head == bottom:
817 # reset the backup information. No logging since the
818 # patch hasn't changed
819 patch.set_bottom(head, backup = True)
820 patch.set_top(top, backup = True)
821
822 else:
823 head_tree = git.get_commit(head).get_tree()
824 bottom_tree = git.get_commit(bottom).get_tree()
825 if head_tree == bottom_tree:
826 # We must just reparent this patch and create a new commit
827 # for it
828 descr = patch.get_description()
829 author_name = patch.get_authname()
830 author_email = patch.get_authemail()
831 author_date = patch.get_authdate()
832 committer_name = patch.get_commname()
833 committer_email = patch.get_commemail()
834
835 top_tree = git.get_commit(top).get_tree()
836
837 top = git.commit(message = descr, parents = [head],
838 cache_update = False,
839 tree_id = top_tree,
840 allowempty = True,
841 author_name = author_name,
842 author_email = author_email,
843 author_date = author_date,
844 committer_name = committer_name,
845 committer_email = committer_email)
846
847 patch.set_bottom(head, backup = True)
848 patch.set_top(top, backup = True)
849
850 self.log_patch(patch, 'push(f)')
851 else:
852 top = head
853 # stop the fast-forwarding, must do a real merge
854 break
855
856 forwarded+=1
857 unapplied.remove(name)
858
859 if forwarded == 0:
860 return 0
861
862 git.switch(top)
863
864 append_strings(self.__applied_file, names[0:forwarded])
865
866 f = file(self.__unapplied_file, 'w+')
867 f.writelines([line + '\n' for line in unapplied])
868 f.close()
869
870 self.__set_current(name)
871
872 return forwarded
873
874 def merged_patches(self, names):
875 """Test which patches were merged upstream by reverse-applying
876 them in reverse order. The function returns the list of
877 patches detected to have been applied. The state of the tree
878 is restored to the original one
879 """
880 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
881 for name in names]
882 patches.reverse()
883
884 merged = []
885 for p in patches:
886 if git.apply_diff(p.get_top(), p.get_bottom()):
887 merged.append(p.get_name())
888 merged.reverse()
889
890 git.reset()
891
892 return merged
893
894 def push_patch(self, name, empty = False):
895 """Pushes a patch on the stack
896 """
897 unapplied = self.get_unapplied()
898 assert(name in unapplied)
899
900 self.__begin_stack_check()
901
902 patch = Patch(name, self.__patch_dir, self.__refs_dir)
903
904 head = git.get_head()
905 bottom = patch.get_bottom()
906 top = patch.get_top()
907
908 ex = None
909 modified = False
910
911 # top != bottom always since we have a commit for each patch
912 if empty:
913 # just make an empty patch (top = bottom = HEAD). This
914 # option is useful to allow undoing already merged
915 # patches. The top is updated by refresh_patch since we
916 # need an empty commit
917 patch.set_bottom(head, backup = True)
918 patch.set_top(head, backup = True)
919 modified = True
920 elif head == bottom:
921 # reset the backup information. No need for logging
922 patch.set_bottom(bottom, backup = True)
923 patch.set_top(top, backup = True)
924
925 git.switch(top)
926 else:
927 # new patch needs to be refreshed.
928 # The current patch is empty after merge.
929 patch.set_bottom(head, backup = True)
930 patch.set_top(head, backup = True)
931
932 # Try the fast applying first. If this fails, fall back to the
933 # three-way merge
934 if not git.apply_diff(bottom, top):
935 # if git.apply_diff() fails, the patch requires a diff3
936 # merge and can be reported as modified
937 modified = True
938
939 # merge can fail but the patch needs to be pushed
940 try:
941 git.merge(bottom, head, top)
942 except git.GitException, ex:
943 print >> sys.stderr, \
944 'The merge failed during "push". ' \
945 'Use "refresh" after fixing the conflicts'
946
947 append_string(self.__applied_file, name)
948
949 unapplied.remove(name)
950 f = file(self.__unapplied_file, 'w+')
951 f.writelines([line + '\n' for line in unapplied])
952 f.close()
953
954 self.__set_current(name)
955
956 # head == bottom case doesn't need to refresh the patch
957 if empty or head != bottom:
958 if not ex:
959 # if the merge was OK and no conflicts, just refresh the patch
960 # The GIT cache was already updated by the merge operation
961 if modified:
962 log = 'push(m)'
963 else:
964 log = 'push'
965 self.refresh_patch(cache_update = False, log = log)
966 else:
967 raise StackException, str(ex)
968
969 return modified
970
971 def undo_push(self):
972 name = self.get_current()
973 assert(name)
974
975 patch = Patch(name, self.__patch_dir, self.__refs_dir)
976 old_bottom = patch.get_old_bottom()
977 old_top = patch.get_old_top()
978
979 # the top of the patch is changed by a push operation only
980 # together with the bottom (otherwise the top was probably
981 # modified by 'refresh'). If they are both unchanged, there
982 # was a fast forward
983 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
984 raise StackException, 'No push undo information available'
985
986 git.reset()
987 self.pop_patch(name)
988 ret = patch.restore_old_boundaries()
989 if ret:
990 self.log_patch(patch, 'undo')
991
992 return ret
993
994 def pop_patch(self, name, keep = False):
995 """Pops the top patch from the stack
996 """
997 applied = self.get_applied()
998 applied.reverse()
999 assert(name in applied)
1000
1001 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1002
1003 # only keep the local changes
1004 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1005 raise StackException, \
1006 'Failed to pop patches while preserving the local changes'
1007
1008 git.switch(patch.get_bottom(), keep)
1009
1010 # save the new applied list
1011 idx = applied.index(name) + 1
1012
1013 popped = applied[:idx]
1014 popped.reverse()
1015 unapplied = popped + self.get_unapplied()
1016
1017 f = file(self.__unapplied_file, 'w+')
1018 f.writelines([line + '\n' for line in unapplied])
1019 f.close()
1020
1021 del applied[:idx]
1022 applied.reverse()
1023
1024 f = file(self.__applied_file, 'w+')
1025 f.writelines([line + '\n' for line in applied])
1026 f.close()
1027
1028 if applied == []:
1029 self.__set_current(None)
1030 else:
1031 self.__set_current(applied[-1])
1032
1033 self.__end_stack_check()
1034
1035 def empty_patch(self, name):
1036 """Returns True if the patch is empty
1037 """
1038 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1039 bottom = patch.get_bottom()
1040 top = patch.get_top()
1041
1042 if bottom == top:
1043 return True
1044 elif git.get_commit(top).get_tree() \
1045 == git.get_commit(bottom).get_tree():
1046 return True
1047
1048 return False
1049
1050 def rename_patch(self, oldname, newname):
1051 applied = self.get_applied()
1052 unapplied = self.get_unapplied()
1053
1054 if oldname == newname:
1055 raise StackException, '"To" name and "from" name are the same'
1056
1057 if newname in applied or newname in unapplied:
1058 raise StackException, 'Patch "%s" already exists' % newname
1059
1060 if oldname in unapplied:
1061 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1062 unapplied[unapplied.index(oldname)] = newname
1063
1064 f = file(self.__unapplied_file, 'w+')
1065 f.writelines([line + '\n' for line in unapplied])
1066 f.close()
1067 elif oldname in applied:
1068 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1069 if oldname == self.get_current():
1070 self.__set_current(newname)
1071
1072 applied[applied.index(oldname)] = newname
1073
1074 f = file(self.__applied_file, 'w+')
1075 f.writelines([line + '\n' for line in applied])
1076 f.close()
1077 else:
1078 raise StackException, 'Unknown patch "%s"' % oldname
1079
1080 def log_patch(self, patch, message):
1081 """Generate a log commit for a patch
1082 """
1083 top = git.get_commit(patch.get_top())
1084 msg = '%s\t%s' % (message, top.get_id_hash())
1085
1086 old_log = patch.get_log()
1087 if old_log:
1088 parents = [old_log]
1089 else:
1090 parents = []
1091
1092 log = git.commit(message = msg, parents = parents,
1093 cache_update = False, tree_id = top.get_tree(),
1094 allowempty = True)
1095 patch.set_log(log)