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