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