Attachment 'untouch.py'

Download

   1 # Copyright 2013 Michael Platings <michael@platin.gs>
   2 #
   3 # Work supported by The Cambridge Crystallographic Data Centre (www.ccdc.cam.ac.uk)
   4 #
   5 # This software may be used and distributed according to the terms
   6 # of the GNU General Public License, incorporated herein by reference.
   7 
   8 """save and restore file modified times.
   9 
  10 This extension is designed to avoid invoking unnecessary builds due to
  11 Mercurial touching files during update & merge or rebase.
  12 
  13 This extension can be used in 3 ways:
  14 
  15 1. When running "hg pull" with the "--rebase" option and without any resulting
  16    conflicts.
  17 
  18   The extension will automatically restore file modified times without any
  19   user input.
  20 
  21   Example::
  22 
  23     hg pull --rebase
  24 
  25 2. To explicitly save file modified times and later restore them.
  26 
  27   Running "hg untouch --save" will record the modified times of files in the
  28   working directory whose status is "added" or "modified" as well as files
  29   that are modified by ancestor changesets whose phase is "draft" or "secret".
  30   
  31   Running "hg untouch" will restore the modified times recorded by previously
  32   running "hg untouch --save".
  33 
  34   Example::
  35 
  36     hg untouch --save
  37     hg update tip
  38     hg merge
  39     hg commit -m "merge"
  40     hg untouch
  41 
  42 3. Any time after running "hg pull"
  43 
  44   The user may run "hg untouch --prepull" to restore the modified times of
  45   certain files. The files will be those that were modified by ancestor
  46   changesets whose phase was "draft" or "secret" at the time "hg pull" was
  47   run.
  48 
  49   The prepull modified times are independent of those created by "hg untouch
  50   --save" so "hg untouch --prepull" can be used independently of "hg untouch".
  51 
  52   Example::
  53   
  54     hg pull
  55     hg update
  56     hg merge
  57     hg commit -m "merge"
  58     hg untouch --prepull
  59 
  60 The extension will never change the modified time of a file if it finds that
  61 the file's content has changed since its modified time was recorded.
  62 
  63 == Hooks ==
  64 
  65 hook_pre_pull: Save the modified times of files in draft and secret
  66   changesets.
  67 
  68 hook_post_pull: If doing "hg pull --rebase" then restore the file modified
  69   times saved by the pre-pull hook.
  70 
  71 == Setup ==
  72 
  73 To enable the extension add the following lines to your hgrc or mercurial.ini::
  74 
  75  [extensions]
  76  untouch = /path/to/untouch.py
  77 
  78  [hooks]
  79  pre-pull.untouch = python:/path/to/untouch.py:hook_pre_pull
  80  post-pull.untouch = python:/path/to/untouch.py:hook_post_pull
  81 
  82 """
  83 
  84 from mercurial import util, commands, phases, scmutil
  85 from mercurial.i18n import _
  86 import os
  87 import cPickle
  88 from datetime import datetime, date
  89 
  90 
  91 def filechecksum(fname):
  92     with open(fname, 'rb') as f:
  93         return util.sha1(f.read()).hexdigest()
  94 
  95 def add_file_details_to_dict(ui, repo, fname, f2th):
  96     fpath = repo.wjoin(fname)
  97     if os.path.isfile(fpath):
  98         mtime = os.stat(fpath).st_mtime
  99         checksum = filechecksum(fpath)
 100         ui.debug(_('untouch: recording ') + fname + _(' modified time: ') + str(datetime.fromtimestamp(mtime)) + '\n')
 101         f2th[fname] = (mtime, checksum)
 102     else:
 103         ui.debug('untouch: not a file: ' + fpath + '\n')
 104 
 105 def trackedfiles(ui, repo, ctx, f2th):
 106     if not ctx:
 107         ui.debug('untouch: trackedfiles not ctx: ' + str(ctx) + ' (returning)\n')
 108         return
 109     if ctx.phase() == phases.public:
 110         ui.debug('untouch: trackedfiles ctx public: ' + str(ctx.description()) + ' (returning)\n')
 111         return
 112     ui.debug('untouch: trackedfiles adding files from ctx: ' + str(ctx.description()) + '\n')
 113     for c in ctx.parents():
 114         trackedfiles(ui, repo, c, f2th)
 115     for fname in ctx.files():
 116         add_file_details_to_dict(ui, repo, fname, f2th)
 117 
 118 def gettrackedfiles(ui, repo):
 119     f2th = {}
 120     for ctx in repo.parents():
 121         trackedfiles(ui, repo, ctx, f2th)
 122     return f2th
 123 
 124 def workingdirfiles(ui, repo, f2th, savemodified, saveadded, saveclean, saveunknown, saveignored):
 125     modified, added, removed, deleted, unknown, ignored, clean = repo.status(clean=saveclean, unknown=saveunknown, ignored=saveignored)
 126     filelists = []
 127     if savemodified: filelists.append(modified)
 128     if saveadded: filelists.append(added)
 129     if saveclean: filelists.append(clean)
 130     if saveunknown: filelists.append(unknown)
 131     if saveignored: filelists.append(ignored)
 132 
 133     for filelist in filelists:
 134         for fname in filelist:
 135             add_file_details_to_dict(ui, repo, fname, f2th)
 136 
 137 def fileschangedinrev(ui, repo, f2th, rev):
 138     for fname in repo[rev].files():
 139         add_file_details_to_dict(ui, repo, fname, f2th)
 140 
 141 def notechangingfiletime(ui, fname, mtime1, mtime2):
 142     today = date.today()
 143     mtime1 = datetime.fromtimestamp(mtime1)
 144     mtime2 = datetime.fromtimestamp(mtime2)
 145     
 146     timeformat = '%H:%M:%S'
 147     datetimeformat = '%Y-%m-%d ' + timeformat
 148 
 149     mtime1 = mtime1.strftime(timeformat if mtime1.date() == today else datetimeformat)
 150     mtime2 = mtime2.strftime(timeformat if mtime2.date() == today else datetimeformat)
 151 
 152     ui.note('untouch: ' + _('changing modified time of ') + fname + _(' from ') + mtime1 + _(' to ') + mtime2 + '\n')
 153 
 154 def getmanualdatapath(repo):
 155     return os.path.join(repo.join('untouch'), 'saved')
 156 
 157 def getpulldatapath(repo):
 158     return os.path.join(repo.join('untouch'), 'prepull')
 159 
 160 def createdatadir(repo):
 161     datadir = repo.join('untouch')
 162     if not os.path.exists(datadir):
 163         os.mkdir(datadir)
 164 
 165 def restore_timestamps_from_file(ui, repo, filetimespath):
 166     with open(filetimespath) as f:
 167         f2th = cPickle.load(f)
 168     
 169     if type(f2th) is not dict:
 170         ui.warn(_('untouch: data file invalid\n'))
 171         return
 172 
 173     ui.debug('untouch: restorefiletimes f2th:' + str(f2th) + '\n')
 174     for fname, th in f2th.iteritems():
 175         fpath = repo.wjoin(fname)
 176         timestamp, hash = th
 177         if os.path.isfile(fpath) and filechecksum(fpath) == hash:
 178             st = os.stat(fpath)
 179             if st.st_mtime != timestamp:
 180                 notechangingfiletime(ui, fname, st.st_mtime, timestamp)
 181                 os.utime(fpath, (st.st_atime, timestamp))
 182 
 183 def hook_pre_pull(ui, repo, hooktype, **kwargs):
 184     createdatadir(repo)
 185     with open(getpulldatapath(repo), 'w') as f:
 186         f2th = gettrackedfiles(ui, repo)
 187         ui.debug('untouch: f2th:' + str(f2th) + '\n')
 188         cPickle.dump(f2th, f)
 189 
 190 def hook_post_pull(ui, repo, hooktype, **kwargs):
 191     opts = kwargs['opts']
 192     if 'rebase' in opts and opts['rebase']:
 193         restore_timestamps_from_file(ui, repo, getpulldatapath(repo))
 194 
 195 def untouch(ui, repo, filename=None, **opts):
 196     """save or restore file modified times.
 197     
 198     With one or more of -srmacui options, save the corresponding file modified
 199     times. If the DEST argument is provided then output to that, otherwise to
 200     .hg/untouch/saved.
 201 
 202     With no arguments, load and restore the file modified times from
 203     .hg/untouch/saved.
 204 
 205     With the SOURCE argument, load and restore the file modified times from
 206     that file.
 207 
 208     If the pre-pull hook (run "hg help untouch -e" for more info) has been set
 209     up, it will automatically save modified times to .hg/untouch/prepull. The
 210     -p option may be used to restore these times.
 211     """
 212 
 213     savedefault  = opts['save']
 214     savemodified = opts['modified'] or savedefault
 215     saveadded    = opts['added'] or savedefault
 216     saveclean    = opts['clean']
 217     saveunknown  = opts['unknown']
 218     saveignored  = opts['ignored']
 219     saverevs     = opts['rev']
 220     if savedefault:
 221         saverevs.append('not public() and ancestors(.)')
 222     saveworkingdir = savemodified or saveadded or saveclean or saveunknown or saveignored
 223     save = saveworkingdir or saverevs
 224     loadprepull = opts['prepull']
 225     load = not save and not loadprepull
 226 
 227     if save and (loadprepull or load):
 228         raise util.Abort(_("cannot specify both saving and loading options"))
 229 
 230     if loadprepull and filename:
 231         raise util.Abort(_("cannot specify loading both from a file and from pre-pull autosave"))
 232 
 233     if save:
 234         if not filename:
 235             createdatadir(repo)
 236             filename = getmanualdatapath(repo)
 237         ui.debug('untouch: saving to ' + filename + '\n')
 238         
 239         with open(filename, 'w') as f:
 240             f2th = {}
 241             if saverevs:
 242                 for rev in scmutil.revrange(repo, saverevs):
 243                     ui.debug('untouch: saving modified times for files modified by revision ' + str(rev) + '\n')
 244                     fileschangedinrev(ui, repo, f2th, rev)
 245             if saveworkingdir:
 246                 ui.debug('untouch: saving modified times for files in the working directory')
 247                 workingdirfiles(ui, repo, f2th, savemodified, saveadded, saveclean, saveunknown, saveignored)
 248             
 249             ui.debug('untouch: f2th:' + str(f2th) + '\n')
 250             cPickle.dump(f2th, f)
 251     else:
 252         if loadprepull:
 253             filename = getpulldatapath(repo)
 254         elif not filename:
 255             filename = getmanualdatapath(repo)
 256         ui.debug('untouch: loading from ' + filename + '\n')
 257         restore_timestamps_from_file(ui, repo, filename)
 258 
 259 cmdtable = {
 260     'untouch': (untouch,
 261                 [('s', 'save',      False,  _('save modified times of added & modified files and files changed in non-public ancestor changesets. Equivalent to -mar "not public() and ancestors(.)"')),
 262                  ('r', 'rev',       [],     _('save modified times of files in the specified revision set'), _('REV')),
 263                  ('m', 'modified',  False,  _('save modified times of modified files')),
 264                  ('a', 'added',     False,  _('save modified times of added files')),
 265                  ('c', 'clean',     False,  _('save modified times of files without changes')),
 266                  ('u', 'unknown',   False,  _('save modified times of unknown (not tracked) files')),
 267                  ('i', 'ignored',   False,  _('save modified times of ignored files')),
 268                  ('p', 'prepull',   False,  _('restore modified times that get automatically recorded before "hg pull"'))],
 269                 '[([-s] [-m] [-a] [-c] [-u] [-i] [-r REV] [DEST]) | -p | SOURCE]')
 270 }
 271 
 272 testedwith = '2.3.2'

Attached Files

To refer to attachments on a page, use attachment:filename, as shown below in the list of files. Do NOT use the URL of the [get] link, since this is subject to change and can break easily.
  • [get | view] (2013-01-16 16:58:09, 10.1 KB) [[attachment:untouch.py]]
 All files | Selected Files: delete move to page copy to page

You are not allowed to attach a file to this page.