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