Fix deletion and move of a hidden patch (gna bug #9244).
[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 # The current StGIT metadata format version.
277 FORMAT_VERSION = 2
278
279 def format_version_key(branch):
280 return 'branch.%s.stgitformatversion' % branch
281
282 def update_to_current_format_version(branch, git_dir):
283 """Update a potentially older StGIT directory structure to the
284 latest version. Note: This function should depend as little as
285 possible on external functions that may change during a format
286 version bump, since it must remain able to process older formats."""
287
288 branch_dir = os.path.join(git_dir, 'patches', branch)
289 def get_format_version():
290 """Return the integer format version number, or None if the
291 branch doesn't have any StGIT metadata at all, of any version."""
292 fv = config.get(format_version_key(branch))
293 if fv:
294 # Great, there's an explicitly recorded format version
295 # number, which means that the branch is initialized and
296 # of that exact version.
297 return int(fv)
298 elif os.path.isdir(os.path.join(branch_dir, 'patches')):
299 # There's a .git/patches/<branch>/patches dirctory, which
300 # means this is an initialized version 1 branch.
301 return 1
302 elif os.path.isdir(branch_dir):
303 # There's a .git/patches/<branch> directory, which means
304 # this is an initialized version 0 branch.
305 return 0
306 else:
307 # The branch doesn't seem to be initialized at all.
308 return None
309 def set_format_version(v):
310 out.info('Upgraded branch %s to format version %d' % (branch, v))
311 config.set(format_version_key(branch), '%d' % v)
312 def mkdir(d):
313 if not os.path.isdir(d):
314 os.makedirs(d)
315 def rm(f):
316 if os.path.exists(f):
317 os.remove(f)
318
319 # Update 0 -> 1.
320 if get_format_version() == 0:
321 mkdir(os.path.join(branch_dir, 'trash'))
322 patch_dir = os.path.join(branch_dir, 'patches')
323 mkdir(patch_dir)
324 refs_dir = os.path.join(git_dir, 'refs', 'patches', branch)
325 mkdir(refs_dir)
326 for patch in (file(os.path.join(branch_dir, 'unapplied')).readlines()
327 + file(os.path.join(branch_dir, 'applied')).readlines()):
328 patch = patch.strip()
329 os.rename(os.path.join(branch_dir, patch),
330 os.path.join(patch_dir, patch))
331 Patch(patch, patch_dir, refs_dir).update_top_ref()
332 set_format_version(1)
333
334 # Update 1 -> 2.
335 if get_format_version() == 1:
336 desc_file = os.path.join(branch_dir, 'description')
337 if os.path.isfile(desc_file):
338 desc = read_string(desc_file)
339 if desc:
340 config.set('branch.%s.description' % branch, desc)
341 rm(desc_file)
342 rm(os.path.join(branch_dir, 'current'))
343 rm(os.path.join(git_dir, 'refs', 'bases', branch))
344 set_format_version(2)
345
346 # Make sure we're at the latest version.
347 if not get_format_version() in [None, FORMAT_VERSION]:
348 raise StackException('Branch %s is at format version %d, expected %d'
349 % (branch, get_format_version(), FORMAT_VERSION))
350
351 class Series(StgitObject):
352 """Class including the operations on series
353 """
354 def __init__(self, name = None):
355 """Takes a series name as the parameter.
356 """
357 try:
358 if name:
359 self.__name = name
360 else:
361 self.__name = git.get_head_file()
362 self.__base_dir = basedir.get()
363 except git.GitException, ex:
364 raise StackException, 'GIT tree not initialised: %s' % ex
365
366 self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name))
367
368 # Update the branch to the latest format version if it is
369 # initialized, but don't touch it if it isn't.
370 update_to_current_format_version(self.__name, self.__base_dir)
371
372 self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches',
373 self.__name)
374
375 self.__applied_file = os.path.join(self._dir(), 'applied')
376 self.__unapplied_file = os.path.join(self._dir(), 'unapplied')
377 self.__hidden_file = os.path.join(self._dir(), 'hidden')
378
379 # where this series keeps its patches
380 self.__patch_dir = os.path.join(self._dir(), 'patches')
381
382 # trash directory
383 self.__trash_dir = os.path.join(self._dir(), 'trash')
384
385 def __patch_name_valid(self, name):
386 """Raise an exception if the patch name is not valid.
387 """
388 if not name or re.search('[^\w.-]', name):
389 raise StackException, 'Invalid patch name: "%s"' % name
390
391 def get_branch(self):
392 """Return the branch name for the Series object
393 """
394 return self.__name
395
396 def get_patch(self, name):
397 """Return a Patch object for the given name
398 """
399 return Patch(name, self.__patch_dir, self.__refs_dir)
400
401 def get_current_patch(self):
402 """Return a Patch object representing the topmost patch, or
403 None if there is no such patch."""
404 crt = self.get_current()
405 if not crt:
406 return None
407 return Patch(crt, self.__patch_dir, self.__refs_dir)
408
409 def get_current(self):
410 """Return the name of the topmost patch, or None if there is
411 no such patch."""
412 try:
413 applied = self.get_applied()
414 except StackException:
415 # No "applied" file: branch is not initialized.
416 return None
417 try:
418 return applied[-1]
419 except IndexError:
420 # No patches applied.
421 return None
422
423 def get_applied(self):
424 if not os.path.isfile(self.__applied_file):
425 raise StackException, 'Branch "%s" not initialised' % self.__name
426 f = file(self.__applied_file)
427 names = [line.strip() for line in f.readlines()]
428 f.close()
429 return names
430
431 def get_unapplied(self):
432 if not os.path.isfile(self.__unapplied_file):
433 raise StackException, 'Branch "%s" not initialised' % self.__name
434 f = file(self.__unapplied_file)
435 names = [line.strip() for line in f.readlines()]
436 f.close()
437 return names
438
439 def get_hidden(self):
440 if not os.path.isfile(self.__hidden_file):
441 return []
442 f = file(self.__hidden_file)
443 names = [line.strip() for line in f.readlines()]
444 f.close()
445 return names
446
447 def get_base(self):
448 # Return the parent of the bottommost patch, if there is one.
449 if os.path.isfile(self.__applied_file):
450 bottommost = file(self.__applied_file).readline().strip()
451 if bottommost:
452 return self.get_patch(bottommost).get_bottom()
453 # No bottommost patch, so just return HEAD
454 return git.get_head()
455
456 def get_head(self):
457 """Return the head of the branch
458 """
459 crt = self.get_current_patch()
460 if crt:
461 return crt.get_top()
462 else:
463 return self.get_base()
464
465 def get_protected(self):
466 return os.path.isfile(os.path.join(self._dir(), 'protected'))
467
468 def protect(self):
469 protect_file = os.path.join(self._dir(), 'protected')
470 if not os.path.isfile(protect_file):
471 create_empty_file(protect_file)
472
473 def unprotect(self):
474 protect_file = os.path.join(self._dir(), 'protected')
475 if os.path.isfile(protect_file):
476 os.remove(protect_file)
477
478 def __branch_descr(self):
479 return 'branch.%s.description' % self.get_branch()
480
481 def get_description(self):
482 return config.get(self.__branch_descr()) or ''
483
484 def set_description(self, line):
485 if line:
486 config.set(self.__branch_descr(), line)
487 else:
488 config.unset(self.__branch_descr())
489
490 def get_parent_remote(self):
491 value = config.get('branch.%s.remote' % self.__name)
492 if value:
493 return value
494 elif 'origin' in git.remotes_list():
495 out.note(('No parent remote declared for stack "%s",'
496 ' defaulting to "origin".' % self.__name),
497 ('Consider setting "branch.%s.remote" and'
498 ' "branch.%s.merge" with "git repo-config".'
499 % (self.__name, self.__name)))
500 return 'origin'
501 else:
502 raise StackException, 'Cannot find a parent remote for "%s"' % self.__name
503
504 def __set_parent_remote(self, remote):
505 value = config.set('branch.%s.remote' % self.__name, remote)
506
507 def get_parent_branch(self):
508 value = config.get('branch.%s.stgit.parentbranch' % self.__name)
509 if value:
510 return value
511 elif git.rev_parse('heads/origin'):
512 out.note(('No parent branch declared for stack "%s",'
513 ' defaulting to "heads/origin".' % self.__name),
514 ('Consider setting "branch.%s.stgit.parentbranch"'
515 ' with "git repo-config".' % self.__name))
516 return 'heads/origin'
517 else:
518 raise StackException, 'Cannot find a parent branch for "%s"' % self.__name
519
520 def __set_parent_branch(self, name):
521 if config.get('branch.%s.remote' % self.__name):
522 # Never set merge if remote is not set to avoid
523 # possibly-erroneous lookups into 'origin'
524 config.set('branch.%s.merge' % self.__name, name)
525 config.set('branch.%s.stgit.parentbranch' % self.__name, name)
526
527 def set_parent(self, remote, localbranch):
528 if localbranch:
529 self.__set_parent_remote(remote)
530 self.__set_parent_branch(localbranch)
531 # We'll enforce this later
532 # else:
533 # raise StackException, 'Parent branch (%s) should be specified for %s' % localbranch, self.__name
534
535 def __patch_is_current(self, patch):
536 return patch.get_name() == self.get_current()
537
538 def patch_applied(self, name):
539 """Return true if the patch exists in the applied list
540 """
541 return name in self.get_applied()
542
543 def patch_unapplied(self, name):
544 """Return true if the patch exists in the unapplied list
545 """
546 return name in self.get_unapplied()
547
548 def patch_hidden(self, name):
549 """Return true if the patch is hidden.
550 """
551 return name in self.get_hidden()
552
553 def patch_exists(self, name):
554 """Return true if there is a patch with the given name, false
555 otherwise."""
556 return self.patch_applied(name) or self.patch_unapplied(name)
557
558 def head_top_equal(self):
559 """Return true if the head and the top are the same
560 """
561 crt = self.get_current_patch()
562 if not crt:
563 # we don't care, no patches applied
564 return True
565 return git.get_head() == crt.get_top()
566
567 def is_initialised(self):
568 """Checks if series is already initialised
569 """
570 return bool(config.get(format_version_key(self.get_branch())))
571
572 def init(self, create_at=False, parent_remote=None, parent_branch=None):
573 """Initialises the stgit series
574 """
575 if self.is_initialised():
576 raise StackException, '%s already initialized' % self.get_branch()
577 for d in [self._dir(), self.__refs_dir]:
578 if os.path.exists(d):
579 raise StackException, '%s already exists' % d
580
581 if (create_at!=False):
582 git.create_branch(self.__name, create_at)
583
584 os.makedirs(self.__patch_dir)
585
586 self.set_parent(parent_remote, parent_branch)
587
588 self.create_empty_field('applied')
589 self.create_empty_field('unapplied')
590 os.makedirs(self.__refs_dir)
591 self._set_field('orig-base', git.get_head())
592
593 config.set(format_version_key(self.get_branch()), str(FORMAT_VERSION))
594
595 def rename(self, to_name):
596 """Renames a series
597 """
598 to_stack = Series(to_name)
599
600 if to_stack.is_initialised():
601 raise StackException, '"%s" already exists' % to_stack.get_branch()
602
603 git.rename_branch(self.__name, to_name)
604
605 if os.path.isdir(self._dir()):
606 rename(os.path.join(self.__base_dir, 'patches'),
607 self.__name, to_stack.__name)
608 if os.path.exists(self.__refs_dir):
609 rename(os.path.join(self.__base_dir, 'refs', 'patches'),
610 self.__name, to_stack.__name)
611
612 # Rename the config section
613 config.rename_section("branch.%s" % self.__name,
614 "branch.%s" % to_name)
615
616 self.__init__(to_name)
617
618 def clone(self, target_series):
619 """Clones a series
620 """
621 try:
622 # allow cloning of branches not under StGIT control
623 base = self.get_base()
624 except:
625 base = git.get_head()
626 Series(target_series).init(create_at = base)
627 new_series = Series(target_series)
628
629 # generate an artificial description file
630 new_series.set_description('clone of "%s"' % self.__name)
631
632 # clone self's entire series as unapplied patches
633 try:
634 # allow cloning of branches not under StGIT control
635 applied = self.get_applied()
636 unapplied = self.get_unapplied()
637 patches = applied + unapplied
638 patches.reverse()
639 except:
640 patches = applied = unapplied = []
641 for p in patches:
642 patch = self.get_patch(p)
643 newpatch = new_series.new_patch(p, message = patch.get_description(),
644 can_edit = False, unapplied = True,
645 bottom = patch.get_bottom(),
646 top = patch.get_top(),
647 author_name = patch.get_authname(),
648 author_email = patch.get_authemail(),
649 author_date = patch.get_authdate())
650 if patch.get_log():
651 out.info('Setting log to %s' % patch.get_log())
652 newpatch.set_log(patch.get_log())
653 else:
654 out.info('No log for %s' % p)
655
656 # fast forward the cloned series to self's top
657 new_series.forward_patches(applied)
658
659 # Clone parent informations
660 value = config.get('branch.%s.remote' % self.__name)
661 if value:
662 config.set('branch.%s.remote' % target_series, value)
663
664 value = config.get('branch.%s.merge' % self.__name)
665 if value:
666 config.set('branch.%s.merge' % target_series, value)
667
668 value = config.get('branch.%s.stgit.parentbranch' % self.__name)
669 if value:
670 config.set('branch.%s.stgit.parentbranch' % target_series, value)
671
672 def delete(self, force = False):
673 """Deletes an stgit series
674 """
675 if self.is_initialised():
676 patches = self.get_unapplied() + self.get_applied()
677 if not force and patches:
678 raise StackException, \
679 'Cannot delete: the series still contains patches'
680 for p in patches:
681 Patch(p, self.__patch_dir, self.__refs_dir).delete()
682
683 # remove the trash directory
684 for fname in os.listdir(self.__trash_dir):
685 os.remove(os.path.join(self.__trash_dir, fname))
686 os.rmdir(self.__trash_dir)
687
688 # FIXME: find a way to get rid of those manual removals
689 # (move functionality to StgitObject ?)
690 if os.path.exists(self.__applied_file):
691 os.remove(self.__applied_file)
692 if os.path.exists(self.__unapplied_file):
693 os.remove(self.__unapplied_file)
694 if os.path.exists(self.__hidden_file):
695 os.remove(self.__hidden_file)
696 if os.path.exists(self._dir()+'/orig-base'):
697 os.remove(self._dir()+'/orig-base')
698
699 if not os.listdir(self.__patch_dir):
700 os.rmdir(self.__patch_dir)
701 else:
702 out.warn('Patch directory %s is not empty' % self.__patch_dir)
703
704 try:
705 os.removedirs(self._dir())
706 except OSError:
707 raise StackException('Series directory %s is not empty'
708 % self._dir())
709
710 try:
711 os.removedirs(self.__refs_dir)
712 except OSError:
713 out.warn('Refs directory %s is not empty' % self.__refs_dir)
714
715 # Cleanup parent informations
716 # FIXME: should one day make use of git-config --section-remove,
717 # scheduled for 1.5.1
718 config.unset('branch.%s.remote' % self.__name)
719 config.unset('branch.%s.merge' % self.__name)
720 config.unset('branch.%s.stgit.parentbranch' % self.__name)
721
722 def refresh_patch(self, files = None, message = None, edit = False,
723 show_patch = False,
724 cache_update = True,
725 author_name = None, author_email = None,
726 author_date = None,
727 committer_name = None, committer_email = None,
728 backup = False, sign_str = None, log = 'refresh'):
729 """Generates a new commit for the given patch
730 """
731 name = self.get_current()
732 if not name:
733 raise StackException, 'No patches applied'
734
735 patch = Patch(name, self.__patch_dir, self.__refs_dir)
736
737 descr = patch.get_description()
738 if not (message or descr):
739 edit = True
740 descr = ''
741 elif message:
742 descr = message
743
744 if not message and edit:
745 descr = edit_file(self, descr.rstrip(), \
746 'Please edit the description for patch "%s" ' \
747 'above.' % name, show_patch)
748
749 if not author_name:
750 author_name = patch.get_authname()
751 if not author_email:
752 author_email = patch.get_authemail()
753 if not author_date:
754 author_date = patch.get_authdate()
755 if not committer_name:
756 committer_name = patch.get_commname()
757 if not committer_email:
758 committer_email = patch.get_commemail()
759
760 if sign_str:
761 descr = descr.rstrip()
762 if descr.find("\nSigned-off-by:") < 0 \
763 and descr.find("\nAcked-by:") < 0:
764 descr = descr + "\n"
765
766 descr = '%s\n%s: %s <%s>\n' % (descr, sign_str,
767 committer_name, committer_email)
768
769 bottom = patch.get_bottom()
770
771 commit_id = git.commit(files = files,
772 message = descr, parents = [bottom],
773 cache_update = cache_update,
774 allowempty = True,
775 author_name = author_name,
776 author_email = author_email,
777 author_date = author_date,
778 committer_name = committer_name,
779 committer_email = committer_email)
780
781 patch.set_bottom(bottom, backup = backup)
782 patch.set_top(commit_id, backup = backup)
783 patch.set_description(descr)
784 patch.set_authname(author_name)
785 patch.set_authemail(author_email)
786 patch.set_authdate(author_date)
787 patch.set_commname(committer_name)
788 patch.set_commemail(committer_email)
789
790 if log:
791 self.log_patch(patch, log)
792
793 return commit_id
794
795 def undo_refresh(self):
796 """Undo the patch boundaries changes caused by 'refresh'
797 """
798 name = self.get_current()
799 assert(name)
800
801 patch = Patch(name, self.__patch_dir, self.__refs_dir)
802 old_bottom = patch.get_old_bottom()
803 old_top = patch.get_old_top()
804
805 # the bottom of the patch is not changed by refresh. If the
806 # old_bottom is different, there wasn't any previous 'refresh'
807 # command (probably only a 'push')
808 if old_bottom != patch.get_bottom() or old_top == patch.get_top():
809 raise StackException, 'No undo information available'
810
811 git.reset(tree_id = old_top, check_out = False)
812 if patch.restore_old_boundaries():
813 self.log_patch(patch, 'undo')
814
815 def new_patch(self, name, message = None, can_edit = True,
816 unapplied = False, show_patch = False,
817 top = None, bottom = None,
818 author_name = None, author_email = None, author_date = None,
819 committer_name = None, committer_email = None,
820 before_existing = False, refresh = True):
821 """Creates a new patch
822 """
823
824 if name != None:
825 self.__patch_name_valid(name)
826 if self.patch_applied(name) or self.patch_unapplied(name):
827 raise StackException, 'Patch "%s" already exists' % name
828
829 if not message and can_edit:
830 descr = edit_file(
831 self, None,
832 'Please enter the description for the patch above.',
833 show_patch)
834 else:
835 descr = message
836
837 head = git.get_head()
838
839 if name == None:
840 name = make_patch_name(descr, self.patch_exists)
841
842 patch = Patch(name, self.__patch_dir, self.__refs_dir)
843 patch.create()
844
845 if bottom:
846 patch.set_bottom(bottom)
847 else:
848 patch.set_bottom(head)
849 if top:
850 patch.set_top(top)
851 else:
852 patch.set_top(head)
853
854 patch.set_description(descr)
855 patch.set_authname(author_name)
856 patch.set_authemail(author_email)
857 patch.set_authdate(author_date)
858 patch.set_commname(committer_name)
859 patch.set_commemail(committer_email)
860
861 if unapplied:
862 self.log_patch(patch, 'new')
863
864 patches = [patch.get_name()] + self.get_unapplied()
865
866 f = file(self.__unapplied_file, 'w+')
867 f.writelines([line + '\n' for line in patches])
868 f.close()
869 elif before_existing:
870 self.log_patch(patch, 'new')
871
872 insert_string(self.__applied_file, patch.get_name())
873 else:
874 append_string(self.__applied_file, patch.get_name())
875 if refresh:
876 self.refresh_patch(cache_update = False, log = 'new')
877
878 return patch
879
880 def delete_patch(self, name):
881 """Deletes a patch
882 """
883 self.__patch_name_valid(name)
884 patch = Patch(name, self.__patch_dir, self.__refs_dir)
885
886 if self.__patch_is_current(patch):
887 self.pop_patch(name)
888 elif self.patch_applied(name):
889 raise StackException, 'Cannot remove an applied patch, "%s", ' \
890 'which is not current' % name
891 elif not name in self.get_unapplied():
892 raise StackException, 'Unknown patch "%s"' % name
893
894 # save the commit id to a trash file
895 write_string(os.path.join(self.__trash_dir, name), patch.get_top())
896
897 if self.patch_hidden(name):
898 self.unhide_patch(name)
899
900 patch.delete()
901
902 unapplied = self.get_unapplied()
903 unapplied.remove(name)
904 f = file(self.__unapplied_file, 'w+')
905 f.writelines([line + '\n' for line in unapplied])
906 f.close()
907
908 def forward_patches(self, names):
909 """Try to fast-forward an array of patches.
910
911 On return, patches in names[0:returned_value] have been pushed on the
912 stack. Apply the rest with push_patch
913 """
914 unapplied = self.get_unapplied()
915
916 forwarded = 0
917 top = git.get_head()
918
919 for name in names:
920 assert(name in unapplied)
921
922 patch = Patch(name, self.__patch_dir, self.__refs_dir)
923
924 head = top
925 bottom = patch.get_bottom()
926 top = patch.get_top()
927
928 # top != bottom always since we have a commit for each patch
929 if head == bottom:
930 # reset the backup information. No logging since the
931 # patch hasn't changed
932 patch.set_bottom(head, backup = True)
933 patch.set_top(top, backup = True)
934
935 else:
936 head_tree = git.get_commit(head).get_tree()
937 bottom_tree = git.get_commit(bottom).get_tree()
938 if head_tree == bottom_tree:
939 # We must just reparent this patch and create a new commit
940 # for it
941 descr = patch.get_description()
942 author_name = patch.get_authname()
943 author_email = patch.get_authemail()
944 author_date = patch.get_authdate()
945 committer_name = patch.get_commname()
946 committer_email = patch.get_commemail()
947
948 top_tree = git.get_commit(top).get_tree()
949
950 top = git.commit(message = descr, parents = [head],
951 cache_update = False,
952 tree_id = top_tree,
953 allowempty = True,
954 author_name = author_name,
955 author_email = author_email,
956 author_date = author_date,
957 committer_name = committer_name,
958 committer_email = committer_email)
959
960 patch.set_bottom(head, backup = True)
961 patch.set_top(top, backup = True)
962
963 self.log_patch(patch, 'push(f)')
964 else:
965 top = head
966 # stop the fast-forwarding, must do a real merge
967 break
968
969 forwarded+=1
970 unapplied.remove(name)
971
972 if forwarded == 0:
973 return 0
974
975 git.switch(top)
976
977 append_strings(self.__applied_file, names[0:forwarded])
978
979 f = file(self.__unapplied_file, 'w+')
980 f.writelines([line + '\n' for line in unapplied])
981 f.close()
982
983 return forwarded
984
985 def merged_patches(self, names):
986 """Test which patches were merged upstream by reverse-applying
987 them in reverse order. The function returns the list of
988 patches detected to have been applied. The state of the tree
989 is restored to the original one
990 """
991 patches = [Patch(name, self.__patch_dir, self.__refs_dir)
992 for name in names]
993 patches.reverse()
994
995 merged = []
996 for p in patches:
997 if git.apply_diff(p.get_top(), p.get_bottom()):
998 merged.append(p.get_name())
999 merged.reverse()
1000
1001 git.reset()
1002
1003 return merged
1004
1005 def push_patch(self, name, empty = False):
1006 """Pushes a patch on the stack
1007 """
1008 unapplied = self.get_unapplied()
1009 assert(name in unapplied)
1010
1011 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1012
1013 head = git.get_head()
1014 bottom = patch.get_bottom()
1015 top = patch.get_top()
1016
1017 ex = None
1018 modified = False
1019
1020 # top != bottom always since we have a commit for each patch
1021 if empty:
1022 # just make an empty patch (top = bottom = HEAD). This
1023 # option is useful to allow undoing already merged
1024 # patches. The top is updated by refresh_patch since we
1025 # need an empty commit
1026 patch.set_bottom(head, backup = True)
1027 patch.set_top(head, backup = True)
1028 modified = True
1029 elif head == bottom:
1030 # reset the backup information. No need for logging
1031 patch.set_bottom(bottom, backup = True)
1032 patch.set_top(top, backup = True)
1033
1034 git.switch(top)
1035 else:
1036 # new patch needs to be refreshed.
1037 # The current patch is empty after merge.
1038 patch.set_bottom(head, backup = True)
1039 patch.set_top(head, backup = True)
1040
1041 # Try the fast applying first. If this fails, fall back to the
1042 # three-way merge
1043 if not git.apply_diff(bottom, top):
1044 # if git.apply_diff() fails, the patch requires a diff3
1045 # merge and can be reported as modified
1046 modified = True
1047
1048 # merge can fail but the patch needs to be pushed
1049 try:
1050 git.merge(bottom, head, top, recursive = True)
1051 except git.GitException, ex:
1052 out.error('The merge failed during "push".',
1053 'Use "refresh" after fixing the conflicts or'
1054 ' revert the operation with "push --undo".')
1055
1056 append_string(self.__applied_file, name)
1057
1058 unapplied.remove(name)
1059 f = file(self.__unapplied_file, 'w+')
1060 f.writelines([line + '\n' for line in unapplied])
1061 f.close()
1062
1063 # head == bottom case doesn't need to refresh the patch
1064 if empty or head != bottom:
1065 if not ex:
1066 # if the merge was OK and no conflicts, just refresh the patch
1067 # The GIT cache was already updated by the merge operation
1068 if modified:
1069 log = 'push(m)'
1070 else:
1071 log = 'push'
1072 self.refresh_patch(cache_update = False, log = log)
1073 else:
1074 # we store the correctly merged files only for
1075 # tracking the conflict history. Note that the
1076 # git.merge() operations should always leave the index
1077 # in a valid state (i.e. only stage 0 files)
1078 self.refresh_patch(cache_update = False, log = 'push(c)')
1079 raise StackException, str(ex)
1080
1081 return modified
1082
1083 def undo_push(self):
1084 name = self.get_current()
1085 assert(name)
1086
1087 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1088 old_bottom = patch.get_old_bottom()
1089 old_top = patch.get_old_top()
1090
1091 # the top of the patch is changed by a push operation only
1092 # together with the bottom (otherwise the top was probably
1093 # modified by 'refresh'). If they are both unchanged, there
1094 # was a fast forward
1095 if old_bottom == patch.get_bottom() and old_top != patch.get_top():
1096 raise StackException, 'No undo information available'
1097
1098 git.reset()
1099 self.pop_patch(name)
1100 ret = patch.restore_old_boundaries()
1101 if ret:
1102 self.log_patch(patch, 'undo')
1103
1104 return ret
1105
1106 def pop_patch(self, name, keep = False):
1107 """Pops the top patch from the stack
1108 """
1109 applied = self.get_applied()
1110 applied.reverse()
1111 assert(name in applied)
1112
1113 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1114
1115 if git.get_head_file() == self.get_branch():
1116 if keep and not git.apply_diff(git.get_head(), patch.get_bottom()):
1117 raise StackException(
1118 'Failed to pop patches while preserving the local changes')
1119 git.switch(patch.get_bottom(), keep)
1120 else:
1121 git.set_branch(self.get_branch(), patch.get_bottom())
1122
1123 # save the new applied list
1124 idx = applied.index(name) + 1
1125
1126 popped = applied[:idx]
1127 popped.reverse()
1128 unapplied = popped + self.get_unapplied()
1129
1130 f = file(self.__unapplied_file, 'w+')
1131 f.writelines([line + '\n' for line in unapplied])
1132 f.close()
1133
1134 del applied[:idx]
1135 applied.reverse()
1136
1137 f = file(self.__applied_file, 'w+')
1138 f.writelines([line + '\n' for line in applied])
1139 f.close()
1140
1141 def empty_patch(self, name):
1142 """Returns True if the patch is empty
1143 """
1144 self.__patch_name_valid(name)
1145 patch = Patch(name, self.__patch_dir, self.__refs_dir)
1146 bottom = patch.get_bottom()
1147 top = patch.get_top()
1148
1149 if bottom == top:
1150 return True
1151 elif git.get_commit(top).get_tree() \
1152 == git.get_commit(bottom).get_tree():
1153 return True
1154
1155 return False
1156
1157 def rename_patch(self, oldname, newname):
1158 self.__patch_name_valid(newname)
1159
1160 applied = self.get_applied()
1161 unapplied = self.get_unapplied()
1162
1163 if oldname == newname:
1164 raise StackException, '"To" name and "from" name are the same'
1165
1166 if newname in applied or newname in unapplied:
1167 raise StackException, 'Patch "%s" already exists' % newname
1168
1169 if self.patch_hidden(oldname):
1170 self.unhide_patch(oldname)
1171 was_hidden=True
1172 else:
1173 was_hidden=False
1174
1175 if oldname in unapplied:
1176 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1177 unapplied[unapplied.index(oldname)] = newname
1178
1179 f = file(self.__unapplied_file, 'w+')
1180 f.writelines([line + '\n' for line in unapplied])
1181 f.close()
1182 elif oldname in applied:
1183 Patch(oldname, self.__patch_dir, self.__refs_dir).rename(newname)
1184
1185 applied[applied.index(oldname)] = newname
1186
1187 f = file(self.__applied_file, 'w+')
1188 f.writelines([line + '\n' for line in applied])
1189 f.close()
1190 else:
1191 raise StackException, 'Unknown patch "%s"' % oldname
1192
1193 if was_hidden:
1194 self.hide_patch(newname)
1195
1196 def log_patch(self, patch, message):
1197 """Generate a log commit for a patch
1198 """
1199 top = git.get_commit(patch.get_top())
1200 msg = '%s\t%s' % (message, top.get_id_hash())
1201
1202 old_log = patch.get_log()
1203 if old_log:
1204 parents = [old_log]
1205 else:
1206 parents = []
1207
1208 log = git.commit(message = msg, parents = parents,
1209 cache_update = False, tree_id = top.get_tree(),
1210 allowempty = True)
1211 patch.set_log(log)
1212
1213 def hide_patch(self, name):
1214 """Add the patch to the hidden list.
1215 """
1216 if not self.patch_exists(name):
1217 raise StackException, 'Unknown patch "%s"' % name
1218 elif self.patch_hidden(name):
1219 raise StackException, 'Patch "%s" already hidden' % name
1220
1221 append_string(self.__hidden_file, name)
1222
1223 def unhide_patch(self, name):
1224 """Add the patch to the hidden list.
1225 """
1226 if not self.patch_exists(name):
1227 raise StackException, 'Unknown patch "%s"' % name
1228 hidden = self.get_hidden()
1229 if not name in hidden:
1230 raise StackException, 'Patch "%s" not hidden' % name
1231
1232 hidden.remove(name)
1233
1234 f = file(self.__hidden_file, 'w+')
1235 f.writelines([line + '\n' for line in hidden])
1236 f.close()