]> asedeno.scripts.mit.edu Git - git-svn-keywords.git/blob - git-svn-keywords.py
Add exception handling for stale files
[git-svn-keywords.git] / git-svn-keywords.py
1 #!/usr/bin/python2.6
2 # -*- coding: utf-8 -*-
3
4 # Copyright (c) 2009-2010 Alejandro R. SedeƱo <asedeno@mit.edu>
5
6 # Permission is hereby granted, free of charge, to any person
7 # obtaining a copy of this software and associated documentation files
8 # (the "Software"), to deal in the Software without restriction,
9 # including without limitation the rights to use, copy, modify, merge,
10 # publish, distribute, sublicense, and/or sell copies of the Software,
11 # and to permit persons to whom the Software is furnished to do so,
12 # subject to the following conditions:
13
14 # The above copyright notice and this permission notice shall be
15 # included in all copies or substantial portions of the Software.
16
17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 # SOFTWARE.
25
26 # git svn keyword parsing, populating, and clearing.
27
28 from __future__ import with_statement
29 import errno, os, re, urllib
30 from ConfigParser import ConfigParser
31 from optparse import OptionParser
32 import fnmatch
33 import git
34
35 VERSION = "0.9.2"
36
37 # Where we keep data in the repo.
38 def gsk(g):
39     return os.path.join(g.path, 'svn_keywords')
40
41 #Configuration Data
42 CONFIG = ConfigParser()
43 FILES = ConfigParser()
44 FILEINFO = ConfigParser()
45
46 CONFIG_PATH = ''
47 FILES_PATH = ''
48 FILEINFO_PATH = ''
49
50
51 # Valid keywords:
52 svn_keywords = {'Date': ['Date', 'LastDateChanged'],
53                 'Revision': ['Revision', 'LastChangedRevision', 'Rev'],
54                 'Author': ['Author','LastChangedBy'],
55                 'URL': ['HeadURL', 'URL'],
56                 'Id': ['Id']
57                 }
58
59 # Regular expressions we'll be using to smudge/clean; created as
60 # needed and cached.
61 svn_keywords_re = {}
62 def get_svn_keyword_re(s):
63     if not s in svn_keywords:
64         raise 'Invalid SVN Keyword'
65     if not s in svn_keywords_re:
66         svn_keywords_re[s] = re.compile('\$(' + ('|'.join(svn_keywords[s])) + ')[^$]*\$')
67     return svn_keywords_re[s]
68
69 def conf_right_version():
70     ver = -1
71     if CONFIG.has_option('core', 'version'):
72         ver = CONFIG.get('core', 'version')
73     return ver == VERSION
74
75 def read_file_data():
76     if conf_right_version():
77         FILES.read(FILES_PATH)
78
79 def get_last_rev(path):
80     if not CONFIG.has_section(path):
81         CONFIG.add_section(path)
82
83     lastrev = None
84     if conf_right_version() and CONFIG.has_option(path, 'lastrev'):
85         try:
86             lastrev = CONFIG.getint(path, 'lastrev')
87         except ValueError:
88             lastrev = None
89     return lastrev
90
91
92 # Parse the unhandled log.
93 def _do_parse_unhandled(directory):
94     base = os.path.join(directory)
95     for d in os.listdir(base):
96         subent = os.path.join(base, d)
97         if (d == 'unhandled.log' and os.path.isfile(subent)):
98             rev = None
99             strip_prefix = g.git.config('--get','svn-remote.svn.fetch').split(':')[0]
100             lastrev = get_last_rev(subent)
101             with open(subent, 'r') as f:
102                 # Compile the regular expressions we'll be using here.
103                 re_rev = re.compile("^r(\d+)$")
104                 re_keywords = re.compile("^\s+[-+]file_prop: (\S+) svn:keywords ?(\S*)$")
105
106                 for line in f:
107                     m = re_rev.match(line)
108                     if m:
109                         rev = m.group(1)
110                         continue
111
112                     if (lastrev >= int(rev)):
113                         continue
114
115                     m = re_keywords.match(line)
116                     if m:
117                         path = urllib.unquote(m.group(1))
118                         path = os.path.relpath(path, strip_prefix)
119                         keywords = set(urllib.unquote(m.group(2)).split(' '))
120                         if not FILES.has_section(path):
121                             FILES.add_section(path)
122                         FILES.set(path, rev, keywords)
123             if rev:
124                 lastrev = max(int(rev), lastrev)
125                 CONFIG.set(subent, 'lastrev', lastrev)
126         elif (os.path.isdir(subent)):
127             _do_parse_unhandled(subent)
128
129 def parse_svn_unhandled(g):
130     try:
131         os.mkdir(gsk(g))
132     except os.error, e:
133         if e.errno != errno.EEXIST:
134             raise
135
136     _do_parse_unhandled(os.path.join(g.path, 'svn'))
137     CONFIG.set('core', 'version', VERSION)
138
139     with open(FILES_PATH, 'wb') as f:
140         FILES.write(f)
141
142     with open(CONFIG_PATH, 'wb') as f:
143         CONFIG.write(f)
144
145 def get_path_info(g, path):
146     write_config = False
147
148     # parse ls-tree output and get a blob id for path
149     try:
150         blob = g.git.ls_tree('HEAD', path).split(' ')[2].split("\t")[0]
151     except IndexError:
152         print "%s may be a stale file in your workspace." %  path
153         return None
154
155     # translate that to a commit id
156     if not CONFIG.has_option('BlobToCommit', blob):
157         CONFIG.set('BlobToCommit', blob, g.commits('HEAD', path, 1)[0].id)
158         write_config = True
159     commit = CONFIG.get('BlobToCommit', blob)
160
161     # tranlsate that into an svn revision id
162     if not CONFIG.has_option('CommitToRev', commit):
163         file_rev = g.git.svn('find-rev', commit)
164         if not file_rev:
165             file_rev = "%iM" % find_last_svn_rev('HEAD')
166         CONFIG.set('CommitToRev', commit, file_rev)
167         write_config = True
168     else:
169         file_rev = CONFIG.get('CommitToRev', commit)
170         if file_rev[-1] is 'M':
171             # Rewrite old nnnnnnM commits if they're now available in SVN
172             commit = g.commits('HEAD', path, 1)[0].id
173             file_rev2 = g.git.svn('find-rev', commit)
174             if file_rev2:
175                 CONFIG.set('BlobToCommit', blob, commit)
176                 CONFIG.set('CommitToRev', commit, file_rev2)
177                 write_config = True
178                 file_rev = file_rev2
179
180     # get information about that revision
181     info_dict = {}
182     if file_rev:
183         if not CONFIG.has_option('RevInfo', file_rev):
184             for line in g.git.svn('info', path).split("\n"):
185                 k, v = line.split(": ", 1)
186                 if k == 'Last Changed Date':
187                     info_dict['Date'] = v
188                 elif k == 'Last Changed Author':
189                     info_dict['Author'] = v
190             CONFIG.set('RevInfo', file_rev, info_dict)
191             write_config = True
192         else:
193             info = CONFIG.get('RevInfo', file_rev)
194             info_dict.update(info if type(info) is dict else eval(info))
195
196         if write_config:
197             with open(CONFIG_PATH, 'wb') as f:
198                 CONFIG.write(f)
199
200     info_dict['Revision'] = file_rev
201     return info_dict
202
203 def find_last_svn_rev(treeish, parent=0):
204     svnRev = g.git.svn('find-rev', "%s~%i" % (treeish, parent))
205     if svnRev:
206         return int(svnRev)
207     else:
208         return find_last_svn_rev(treeish, parent+1)
209
210 # Do the work.
211 def smudge(g, options):
212     read_file_data()
213     parse_svn_unhandled(g)
214     rev_head = find_last_svn_rev('HEAD')
215     url_base = g.git.svn('info', '--url')
216
217     FILES.read(FILES_PATH)
218     FILEINFO.read(FILEINFO_PATH)
219
220     ignores = []
221     with open(os.path.join(g.wd,'.git','info','exclude')) as f:
222         for line in f:
223             line = line.rstrip('\n')
224             if line and line[0] != '#':
225                 ignores.append(re.compile(fnmatch.translate(line)))
226
227     paths = FILES.sections()
228     paths.sort()
229     for path in paths:
230         if not os.path.exists(path):
231             continue
232
233         ignore = False
234         for i in ignores:
235             if i.match(path):
236                 ignore = True
237                 break
238         if ignore:
239             continue
240         try:
241             kw_rev = max(filter(lambda x: x <= rev_head, map(int, FILES.options(path))))
242         except ValueError:
243             continue
244
245         info_dict = {}
246         if not options.clean:
247             update_dict = get_path_info(g, path)
248             if not update_dict:
249                 continue
250             info_dict.update(update_dict)
251             info_dict['URL'] = '/'.join([url_base, path])
252             info_dict['Name'] = os.path.basename(path)
253             info_dict['Revision'] = str(max(kw_rev, info_dict['Revision']))
254
255         buf = ''
256         with open(os.path.join(g.wd, path), 'r') as f:
257             buf = f.read()
258
259         keywords = eval(FILES.get(path, str(kw_rev)))
260         for k in keywords:
261             for sk in svn_keywords:
262                 if k in svn_keywords[sk]:
263                     if options.clean:
264                         buf = get_svn_keyword_re(sk).sub('$\\1$', buf)
265                     elif sk == 'Id':
266                         id_str = ' '.join([info_dict['Name'],
267                                            info_dict['Revision'],
268                                            info_dict['Date'],
269                                            info_dict['Author']])
270                         buf = get_svn_keyword_re(sk).sub('$\\1: ' + id_str + ' $', buf)
271                     else:
272                         buf = get_svn_keyword_re(sk).sub('$\\1: ' + info_dict[sk] + ' $', buf)
273
274         with open(os.path.join(g.wd, path), 'w') as f:
275             f.write(buf)
276         if options.verbose:
277             print path + ' [' + ', '.join(keywords) + '] [len: ' + str(len(buf)) +']'
278
279 if __name__ == '__main__':
280
281     parser = OptionParser(version="%prog "+str(VERSION))
282     parser.set_defaults(clean=None)
283     parser.add_option("-s", "--smudge",
284                       action="store_false", dest="clean",
285                       help="Populate svn:keywords.")
286     parser.add_option("-c", "--clean",
287                       action="store_true", dest="clean",
288                       help="Return svn:keywords to pristene state.")
289     parser.add_option("-v", "--verbose",
290                       action="store_true", dest="verbose", default=False)
291     (options, args) = parser.parse_args()
292
293     if (options.clean is None):
294         parser.print_help()
295         exit(0)
296     else:
297         try:
298             g = git.Repo()
299         except git.errors.InvalidGitRepositoryError:
300             print "You are not in a git repository or working directory."
301             exit(1)
302
303         if g.bare:
304             print "This appears to be a bare git repository."
305             exit(1)
306
307         os.chdir(g.wd)
308
309         CONFIG_PATH = os.path.join(gsk(g), 'conf.ini')
310         FILES_PATH = os.path.join(gsk(g), 'files.ini')
311         FILEINFO_PATH = os.path.join(gsk(g), 'fileinfo.ini')
312
313         CONFIG.read(CONFIG_PATH)
314         for section in ['core','CommitToRev','BlobToCommit', 'RevInfo']:
315             if not CONFIG.has_section(section):
316                 CONFIG.add_section(section)
317
318         smudge(g, options)