X-Git-Url: https://git.distorted.org.uk/~mdw/stgit/blobdiff_plain/3a284728a544e34d9db139c3ac589c36275c2416..8fe7e9f08ae6fe49cec66a8034e8495cbf77c6f3:/stgit/stack.py diff --git a/stgit/stack.py b/stgit/stack.py index 0217a7f..69fa03b 100644 --- a/stgit/stack.py +++ b/stgit/stack.py @@ -21,7 +21,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys, os from stgit.utils import * -from stgit import git, basedir +from stgit import git, basedir, templates from stgit.config import config @@ -65,14 +65,14 @@ def __clean_comments(f): f.writelines(lines) def edit_file(series, line, comment, show_patch = True): - fname = '.stgit.msg' - tmpl = os.path.join(basedir.get(), 'patchdescr.tmpl') + fname = '.stgitmsg.txt' + tmpl = templates.get_template('patchdescr.tmpl') f = file(fname, 'w+') if line: print >> f, line - elif os.path.isfile(tmpl): - print >> f, file(tmpl).read().rstrip() + elif tmpl: + print >> f, tmpl, else: print >> f print >> f, __comment_prefix, comment @@ -119,163 +119,172 @@ def edit_file(series, line, comment, show_patch = True): # Classes # -class Patch: +class StgitObject: + """An object with stgit-like properties stored as files in a directory + """ + def _set_dir(self, dir): + self.__dir = dir + def _dir(self): + return self.__dir + + def create_empty_field(self, name): + create_empty_file(os.path.join(self.__dir, name)) + + def _get_field(self, name, multiline = False): + id_file = os.path.join(self.__dir, name) + if os.path.isfile(id_file): + line = read_string(id_file, multiline) + if line == '': + return None + else: + return line + else: + return None + + def _set_field(self, name, value, multiline = False): + fname = os.path.join(self.__dir, name) + if value and value != '': + write_string(fname, value, multiline) + elif os.path.isfile(fname): + os.remove(fname) + + +class Patch(StgitObject): """Basic patch implementation """ def __init__(self, name, series_dir, refs_dir): self.__series_dir = series_dir self.__name = name - self.__dir = os.path.join(self.__series_dir, self.__name) + self._set_dir(os.path.join(self.__series_dir, self.__name)) self.__refs_dir = refs_dir self.__top_ref_file = os.path.join(self.__refs_dir, self.__name) + self.__log_ref_file = os.path.join(self.__refs_dir, + self.__name + '.log') def create(self): - os.mkdir(self.__dir) - create_empty_file(os.path.join(self.__dir, 'bottom')) - create_empty_file(os.path.join(self.__dir, 'top')) + os.mkdir(self._dir()) + self.create_empty_field('bottom') + self.create_empty_field('top') def delete(self): - for f in os.listdir(self.__dir): - os.remove(os.path.join(self.__dir, f)) - os.rmdir(self.__dir) + for f in os.listdir(self._dir()): + os.remove(os.path.join(self._dir(), f)) + os.rmdir(self._dir()) os.remove(self.__top_ref_file) + if os.path.exists(self.__log_ref_file): + os.remove(self.__log_ref_file) def get_name(self): return self.__name def rename(self, newname): - olddir = self.__dir - old_ref_file = self.__top_ref_file + olddir = self._dir() + old_top_ref_file = self.__top_ref_file + old_log_ref_file = self.__log_ref_file self.__name = newname - self.__dir = os.path.join(self.__series_dir, self.__name) + self._set_dir(os.path.join(self.__series_dir, self.__name)) self.__top_ref_file = os.path.join(self.__refs_dir, self.__name) + self.__log_ref_file = os.path.join(self.__refs_dir, + self.__name + '.log') - os.rename(olddir, self.__dir) - os.rename(old_ref_file, self.__top_ref_file) + os.rename(olddir, self._dir()) + os.rename(old_top_ref_file, self.__top_ref_file) + if os.path.exists(old_log_ref_file): + os.rename(old_log_ref_file, self.__log_ref_file) def __update_top_ref(self, ref): write_string(self.__top_ref_file, ref) + def __update_log_ref(self, ref): + write_string(self.__log_ref_file, ref) + def update_top_ref(self): top = self.get_top() if top: self.__update_top_ref(top) - def __get_field(self, name, multiline = False): - id_file = os.path.join(self.__dir, name) - if os.path.isfile(id_file): - line = read_string(id_file, multiline) - if line == '': - return None - else: - return line - else: - return None - - def __set_field(self, name, value, multiline = False): - fname = os.path.join(self.__dir, name) - if value and value != '': - write_string(fname, value, multiline) - elif os.path.isfile(fname): - os.remove(fname) - def get_old_bottom(self): - return self.__get_field('bottom.old') + return self._get_field('bottom.old') def get_bottom(self): - return self.__get_field('bottom') + return self._get_field('bottom') def set_bottom(self, value, backup = False): if backup: - curr = self.__get_field('bottom') - self.__set_field('bottom.old', curr) - self.__set_field('bottom', value) + curr = self._get_field('bottom') + self._set_field('bottom.old', curr) + self._set_field('bottom', value) def get_old_top(self): - return self.__get_field('top.old') + return self._get_field('top.old') def get_top(self): - return self.__get_field('top') + return self._get_field('top') def set_top(self, value, backup = False): if backup: - curr = self.__get_field('top') - self.__set_field('top.old', curr) - self.__set_field('top', value) + curr = self._get_field('top') + self._set_field('top.old', curr) + self._set_field('top', value) self.__update_top_ref(value) def restore_old_boundaries(self): - bottom = self.__get_field('bottom.old') - top = self.__get_field('top.old') + bottom = self._get_field('bottom.old') + top = self._get_field('top.old') if top and bottom: - self.__set_field('bottom', bottom) - self.__set_field('top', top) + self._set_field('bottom', bottom) + self._set_field('top', top) self.__update_top_ref(top) return True else: return False def get_description(self): - return self.__get_field('description', True) + return self._get_field('description', True) def set_description(self, line): - self.__set_field('description', line, True) + self._set_field('description', line, True) def get_authname(self): - return self.__get_field('authname') + return self._get_field('authname') def set_authname(self, name): - if not name: - if config.has_option('stgit', 'authname'): - name = config.get('stgit', 'authname') - elif 'GIT_AUTHOR_NAME' in os.environ: - name = os.environ['GIT_AUTHOR_NAME'] - self.__set_field('authname', name) + self._set_field('authname', name or git.author().name) def get_authemail(self): - return self.__get_field('authemail') + return self._get_field('authemail') - def set_authemail(self, address): - if not address: - if config.has_option('stgit', 'authemail'): - address = config.get('stgit', 'authemail') - elif 'GIT_AUTHOR_EMAIL' in os.environ: - address = os.environ['GIT_AUTHOR_EMAIL'] - self.__set_field('authemail', address) + def set_authemail(self, email): + self._set_field('authemail', email or git.author().email) def get_authdate(self): - return self.__get_field('authdate') + return self._get_field('authdate') def set_authdate(self, date): - if not date and 'GIT_AUTHOR_DATE' in os.environ: - date = os.environ['GIT_AUTHOR_DATE'] - self.__set_field('authdate', date) + self._set_field('authdate', date or git.author().date) def get_commname(self): - return self.__get_field('commname') + return self._get_field('commname') def set_commname(self, name): - if not name: - if config.has_option('stgit', 'commname'): - name = config.get('stgit', 'commname') - elif 'GIT_COMMITTER_NAME' in os.environ: - name = os.environ['GIT_COMMITTER_NAME'] - self.__set_field('commname', name) + self._set_field('commname', name or git.committer().name) def get_commemail(self): - return self.__get_field('commemail') + return self._get_field('commemail') + + def set_commemail(self, email): + self._set_field('commemail', email or git.committer().email) + + def get_log(self): + return self._get_field('log') - def set_commemail(self, address): - if not address: - if config.has_option('stgit', 'commemail'): - address = config.get('stgit', 'commemail') - elif 'GIT_COMMITTER_EMAIL' in os.environ: - address = os.environ['GIT_COMMITTER_EMAIL'] - self.__set_field('commemail', address) + def set_log(self, value, backup = False): + self._set_field('log', value) + self.__update_log_ref(value) -class Series: +class Series(StgitObject): """Class including the operations on series """ def __init__(self, name = None): @@ -290,22 +299,21 @@ class Series: except git.GitException, ex: raise StackException, 'GIT tree not initialised: %s' % ex - self.__series_dir = os.path.join(self.__base_dir, 'patches', - self.__name) + self._set_dir(os.path.join(self.__base_dir, 'patches', self.__name)) self.__refs_dir = os.path.join(self.__base_dir, 'refs', 'patches', self.__name) self.__base_file = os.path.join(self.__base_dir, 'refs', 'bases', self.__name) - self.__applied_file = os.path.join(self.__series_dir, 'applied') - self.__unapplied_file = os.path.join(self.__series_dir, 'unapplied') - self.__current_file = os.path.join(self.__series_dir, 'current') - self.__descr_file = os.path.join(self.__series_dir, 'description') + self.__applied_file = os.path.join(self._dir(), 'applied') + self.__unapplied_file = os.path.join(self._dir(), 'unapplied') + self.__current_file = os.path.join(self._dir(), 'current') + self.__descr_file = os.path.join(self._dir(), 'description') # where this series keeps its patches - self.__patch_dir = os.path.join(self.__series_dir, 'patches') + self.__patch_dir = os.path.join(self._dir(), 'patches') if not os.path.isdir(self.__patch_dir): - self.__patch_dir = self.__series_dir + self.__patch_dir = self._dir() # if no __refs_dir, create and populate it (upgrade old repositories) if self.is_initialised() and not os.path.isdir(self.__refs_dir): @@ -313,6 +321,11 @@ class Series: for patch in self.get_applied() + self.get_unapplied(): self.get_patch(patch).update_top_ref() + # trash directory + self.__trash_dir = os.path.join(self._dir(), 'trash') + if self.is_initialised() and not os.path.isdir(self.__trash_dir): + os.makedirs(self.__trash_dir) + def get_branch(self): """Return the branch name for the Series object """ @@ -321,23 +334,25 @@ class Series: def __set_current(self, name): """Sets the topmost patch """ - if name: - write_string(self.__current_file, name) - else: - create_empty_file(self.__current_file) + self._set_field('current', name) def get_patch(self, name): """Return a Patch object for the given name """ return Patch(name, self.__patch_dir, self.__refs_dir) - def get_current(self): - """Return a Patch object representing the topmost patch - """ - if os.path.isfile(self.__current_file): - name = read_string(self.__current_file) - else: + def get_current_patch(self): + """Return a Patch object representing the topmost patch, or + None if there is no such patch.""" + crt = self.get_current() + if not crt: return None + return Patch(crt, self.__patch_dir, self.__refs_dir) + + def get_current(self): + """Return the name of the topmost patch, or None if there is + no such patch.""" + name = self._get_field('current') if name == '': return None else: @@ -364,37 +379,42 @@ class Series: return self.__base_file def get_protected(self): - return os.path.isfile(os.path.join(self.__series_dir, 'protected')) + return os.path.isfile(os.path.join(self._dir(), 'protected')) def protect(self): - protect_file = os.path.join(self.__series_dir, 'protected') + protect_file = os.path.join(self._dir(), 'protected') if not os.path.isfile(protect_file): create_empty_file(protect_file) def unprotect(self): - protect_file = os.path.join(self.__series_dir, 'protected') + protect_file = os.path.join(self._dir(), 'protected') if os.path.isfile(protect_file): os.remove(protect_file) def get_description(self): - if os.path.isfile(self.__descr_file): - return read_string(self.__descr_file) - else: - return '' + return self._get_field('description') + + def set_description(self, line): + self._set_field('description', line) def __patch_is_current(self, patch): - return patch.get_name() == read_string(self.__current_file) + return patch.get_name() == self.get_current() - def __patch_applied(self, name): + def patch_applied(self, name): """Return true if the patch exists in the applied list """ return name in self.get_applied() - def __patch_unapplied(self, name): + def patch_unapplied(self, name): """Return true if the patch exists in the unapplied list """ return name in self.get_unapplied() + def patch_exists(self, name): + """Return true if there is a patch with the given name, false + otherwise.""" + return self.patch_applied(name) or self.patch_unapplied(name) + def __begin_stack_check(self): """Save the current HEAD into .git/refs/heads/base if the stack is empty @@ -414,12 +434,11 @@ class Series: def head_top_equal(self): """Return true if the head and the top are the same """ - crt = self.get_current() + crt = self.get_current_patch() if not crt: # we don't care, no patches applied return True - return git.get_head() == Patch(crt, self.__patch_dir, - self.__refs_dir).get_top() + return git.get_head() == crt.get_top() def is_initialised(self): """Checks if series is already initialised @@ -445,10 +464,10 @@ class Series: create_dirs(bases_dir) - create_empty_file(self.__applied_file) - create_empty_file(self.__unapplied_file) - create_empty_file(self.__descr_file) - os.makedirs(os.path.join(self.__series_dir, 'patches')) + self.create_empty_field('applied') + self.create_empty_field('unapplied') + self.create_empty_field('description') + os.makedirs(os.path.join(self._dir(), 'patches')) os.makedirs(self.__refs_dir) self.__begin_stack_check() @@ -457,15 +476,15 @@ class Series: unconvert to place the patches in the same directory with series control files """ - if self.__patch_dir == self.__series_dir: + if self.__patch_dir == self._dir(): print 'Converting old-style to new-style...', sys.stdout.flush() - self.__patch_dir = os.path.join(self.__series_dir, 'patches') + self.__patch_dir = os.path.join(self._dir(), 'patches') os.makedirs(self.__patch_dir) for p in self.get_applied() + self.get_unapplied(): - src = os.path.join(self.__series_dir, p) + src = os.path.join(self._dir(), p) dest = os.path.join(self.__patch_dir, p) os.rename(src, dest) @@ -477,7 +496,7 @@ class Series: for p in self.get_applied() + self.get_unapplied(): src = os.path.join(self.__patch_dir, p) - dest = os.path.join(self.__series_dir, p) + dest = os.path.join(self._dir(), p) os.rename(src, dest) if not os.listdir(self.__patch_dir): @@ -486,7 +505,7 @@ class Series: else: print 'Patch directory %s is not empty.' % self.__name - self.__patch_dir = self.__series_dir + self.__patch_dir = self._dir() def rename(self, to_name): """Renames a series @@ -500,7 +519,7 @@ class Series: git.rename_branch(self.__name, to_name) - if os.path.isdir(self.__series_dir): + if os.path.isdir(self._dir()): rename(os.path.join(self.__base_dir, 'patches'), self.__name, to_stack.__name) if os.path.exists(self.__base_file): @@ -520,7 +539,7 @@ class Series: new_series = Series(target_series) # generate an artificial description file - write_string(new_series.__descr_file, 'clone of "%s"' % self.__name) + new_series.set_description('clone of "%s"' % self.__name) # clone self's entire series as unapplied patches patches = self.get_applied() + self.get_unapplied() @@ -549,6 +568,13 @@ class Series: for p in patches: Patch(p, self.__patch_dir, self.__refs_dir).delete() + # remove the trash directory + for fname in os.listdir(self.__trash_dir): + os.remove(fname) + os.rmdir(self.__trash_dir) + + # FIXME: find a way to get rid of those manual removals + # (move functionnality to StgitObject ?) if os.path.exists(self.__applied_file): os.remove(self.__applied_file) if os.path.exists(self.__unapplied_file): @@ -561,7 +587,7 @@ class Series: os.rmdir(self.__patch_dir) else: print 'Patch directory %s is not empty.' % self.__name - if not os.listdir(self.__series_dir): + if not os.listdir(self._dir()): remove_dirs(os.path.join(self.__base_dir, 'patches'), self.__name) else: @@ -582,7 +608,7 @@ class Series: author_name = None, author_email = None, author_date = None, committer_name = None, committer_email = None, - backup = False): + backup = False, sign_str = None, log = 'refresh'): """Generates a new commit for the given patch """ name = self.get_current() @@ -614,6 +640,10 @@ class Series: if not committer_email: committer_email = patch.get_commemail() + if sign_str: + descr = '%s\n%s: %s <%s>\n' % (descr.rstrip(), sign_str, + committer_name, committer_email) + bottom = patch.get_bottom() commit_id = git.commit(files = files, @@ -635,6 +665,9 @@ class Series: patch.set_commname(committer_name) patch.set_commemail(committer_email) + if log: + self.log_patch(patch, log) + return commit_id def undo_refresh(self): @@ -654,17 +687,18 @@ class Series: raise StackException, 'No refresh undo information available' git.reset(tree_id = old_top, check_out = False) - patch.restore_old_boundaries() + if patch.restore_old_boundaries(): + self.log_patch(patch, 'undo') def new_patch(self, name, message = None, can_edit = True, unapplied = False, show_patch = False, top = None, bottom = None, author_name = None, author_email = None, author_date = None, committer_name = None, committer_email = None, - before_existing = False): + before_existing = False, refresh = True): """Creates a new patch """ - if self.__patch_applied(name) or self.__patch_unapplied(name): + if self.patch_applied(name) or self.patch_unapplied(name): raise StackException, 'Patch "%s" already exists' % name if not message and can_edit: @@ -698,21 +732,24 @@ class Series: patch.set_commemail(committer_email) if unapplied: + self.log_patch(patch, 'new') + patches = [patch.get_name()] + self.get_unapplied() f = file(self.__unapplied_file, 'w+') f.writelines([line + '\n' for line in patches]) f.close() - else: - if before_existing: - insert_string(self.__applied_file, patch.get_name()) - if not self.get_current(): - self.__set_current(name) - else: - append_string(self.__applied_file, patch.get_name()) - self.__set_current(name) + elif before_existing: + self.log_patch(patch, 'new') - self.refresh_patch(cache_update = False) + insert_string(self.__applied_file, patch.get_name()) + if not self.get_current(): + self.__set_current(name) + else: + append_string(self.__applied_file, patch.get_name()) + self.__set_current(name) + if refresh: + self.refresh_patch(cache_update = False, log = 'new') def delete_patch(self, name): """Deletes a patch @@ -721,12 +758,15 @@ class Series: if self.__patch_is_current(patch): self.pop_patch(name) - elif self.__patch_applied(name): + elif self.patch_applied(name): raise StackException, 'Cannot remove an applied patch, "%s", ' \ 'which is not current' % name elif not name in self.get_unapplied(): raise StackException, 'Unknown patch "%s"' % name + # save the commit id to a trash file + write_string(os.path.join(self.__trash_dir, name), patch.get_top()) + patch.delete() unapplied = self.get_unapplied() @@ -759,7 +799,8 @@ class Series: # top != bottom always since we have a commit for each patch if head == bottom: - # reset the backup information + # reset the backup information. No logging since the + # patch hasn't changed patch.set_bottom(head, backup = True) patch.set_top(top, backup = True) @@ -790,6 +831,8 @@ class Series: patch.set_bottom(head, backup = True) patch.set_top(top, backup = True) + + self.log_patch(patch, 'push(f)') else: top = head # stop the fast-forwarding, must do a real merge @@ -860,7 +903,7 @@ class Series: patch.set_top(head, backup = True) modified = True elif head == bottom: - # reset the backup information + # reset the backup information. No need for logging patch.set_bottom(bottom, backup = True) patch.set_top(top, backup = True) @@ -900,7 +943,11 @@ class Series: if not ex: # if the merge was OK and no conflicts, just refresh the patch # The GIT cache was already updated by the merge operation - self.refresh_patch(cache_update = False) + if modified: + log = 'push(m)' + else: + log = 'push' + self.refresh_patch(cache_update = False, log = log) else: raise StackException, str(ex) @@ -923,9 +970,13 @@ class Series: git.reset() self.pop_patch(name) - return patch.restore_old_boundaries() + ret = patch.restore_old_boundaries() + if ret: + self.log_patch(patch, 'undo') + + return ret - def pop_patch(self, name): + def pop_patch(self, name, keep = False): """Pops the top patch from the stack """ applied = self.get_applied() @@ -934,7 +985,12 @@ class Series: patch = Patch(name, self.__patch_dir, self.__refs_dir) - git.switch(patch.get_bottom()) + # only keep the local changes + if keep and not git.apply_diff(git.get_head(), patch.get_bottom()): + raise StackException, \ + 'Failed to pop patches while preserving the local changes' + + git.switch(patch.get_bottom(), keep) # save the new applied list idx = applied.index(name) + 1 @@ -1005,3 +1061,20 @@ class Series: f.close() else: raise StackException, 'Unknown patch "%s"' % oldname + + def log_patch(self, patch, message): + """Generate a log commit for a patch + """ + top = git.get_commit(patch.get_top()) + msg = '%s\t%s' % (message, top.get_id_hash()) + + old_log = patch.get_log() + if old_log: + parents = [old_log] + else: + parents = [] + + log = git.commit(message = msg, parents = parents, + cache_update = False, tree_id = top.get_tree(), + allowempty = True) + patch.set_log(log)