]> asedeno.scripts.mit.edu Git - git-svn-keywords.git/blob - git-svn-keywords.py
dd2ec130dd77c6227b934ddc798ba049312574da
[git-svn-keywords.git] / git-svn-keywords.py
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # Copyright (c) 2009 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 from fnmatch import fnmatch
33 import git
34
35 VERSION = "0.9"
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 # Parse the unhandled log.
70 def _do_parse_unhandled(f, lastrev=None):
71     # Compile the regular expressions we'll be using here.
72     re_rev = re.compile("^r(\d+)$")
73     re_keywords = re.compile("^\s+[-+]file_prop: (\S+) svn:keywords ?(\S*)$")
74
75     rev = None
76     for line in f:
77         m = re_rev.match(line)
78         if m:
79             rev = m.group(1)
80             continue
81
82         if (lastrev >= int(rev)):
83             continue
84
85         m = re_keywords.match(line)
86         if m:
87             path = urllib.unquote(m.group(1))
88             keywords = set(urllib.unquote(m.group(2)).split(' '))
89             if not FILES.has_section(path):
90                 FILES.add_section(path)
91             FILES.set(path, rev, keywords)
92
93     lastrev = max(int(rev), lastrev)
94     CONFIG.set('core', 'lastrev', lastrev)
95     CONFIG.set('core', 'version', VERSION)
96
97 def parse_svn_unhandled(g):
98     try:
99         os.mkdir(gsk(g))
100     except os.error, e:
101         if e.errno != errno.EEXIST:
102             raise
103
104     ver = -1
105     if CONFIG.has_option('core', 'version'):
106         ver = CONFIG.get('core', 'version')
107
108     lastrev = None
109     if ver == VERSION:
110         FILES.read(FILES_PATH)
111         if CONFIG.has_option('core', 'lastrev'):
112             lastrev = CONFIG.getint('core', 'lastrev')
113
114     for remote in ['svn', 'svn/refs/remotes']:
115         base = os.path.join(g.path,remote)
116         for d in os.listdir(base):
117             if os.path.isdir(os.path.join(base,d)):
118                 unhandled = os.path.join(base,d,'unhandled.log')
119                 if os.path.isfile(unhandled):
120                     with open(unhandled, 'r') as f:
121                         _do_parse_unhandled(f, lastrev=lastrev)
122
123     with open(FILES_PATH, 'wb') as f:
124         FILES.write(f)
125
126     with open(CONFIG_PATH, 'wb') as f:
127         CONFIG.write(f)
128
129 def get_path_info(g, path):
130     write_config = False
131
132     # parse ls-tree output and get a blob id for path
133     blob = g.git.ls_tree('HEAD', path).split(' ')[2].split("\t")[0]
134
135     # translate that to a commit id
136     if not CONFIG.has_option('BlobToCommit', blob):
137         CONFIG.set('BlobToCommit', blob, g.commits('HEAD', path, 1)[0].id)
138         write_config = True
139     commit = CONFIG.get('BlobToCommit', blob)
140
141     # tranlsate that into an svn revision id
142     if not CONFIG.has_option('CommitToRev', commit):
143         CONFIG.set('CommitToRev',commit,g.git.svn('find-rev', commit))
144         write_config = True
145     file_rev = CONFIG.get('CommitToRev', commit)
146
147     # get information about that revision
148     info_dict = {}
149     if not CONFIG.has_option('RevInfo', file_rev):
150         for line in g.git.svn('info', path).split("\n"):
151             k, v = line.split(": ", 1)
152             if k == 'Last Changed Date':
153                 info_dict['Date'] = v
154             elif k == 'Last Changed Author':
155                 info_dict['Author'] = v
156         CONFIG.set('RevInfo', file_rev, info_dict)
157         write_config = True
158     else:
159         info = CONFIG.get('RevInfo', file_rev)
160         info_dict.update(info if type(info) is dict else eval(info))
161
162     if write_config:
163         with open(CONFIG_PATH, 'wb') as f:
164             CONFIG.write(f)
165
166     info_dict['Revision'] = file_rev
167     return info_dict
168
169 def find_last_svn_rev(treeish, parent=0):
170     svnRev = g.git.svn('find-rev', "%s~%i" % (treeish, parent))
171     if svnRev:
172         return int(svnRev)
173     else:
174         return find_last_svn_rev(treeish, parent+1)
175
176 # Do the work.
177 def smudge(g, options):
178     parse_svn_unhandled(g)
179     rev_head = find_last_svn_rev('HEAD')
180     url_base = g.git.svn('info', '--url')
181
182     FILES.read(FILES_PATH)
183     FILEINFO.read(FILEINFO_PATH)
184
185     ignores = []
186     with open(os.path.join(g.wd,'.git','info','exclude')) as f:
187         for line in f:
188             if line[0] != '#':
189                 ignores.append(line.rstrip('\n'))
190
191     paths = FILES.sections()
192     paths.sort()
193     for path in paths:
194         if not os.path.exists(path):
195             continue
196
197         ignore = False
198         for i in ignores:
199             if fnmatch(path, i):
200                 ignore = True
201         if ignore:
202             continue
203         try:
204             kw_rev = max(filter(lambda x: x <= rev_head, map(int, FILES.options(path))))
205         except ValueError:
206             continue
207
208         info_dict = {}
209         if not options.clean:
210             info_dict.update(get_path_info(g, path))
211             info_dict['URL'] = '/'.join([url_base, path])
212             info_dict['Name'] = os.path.basename(path)
213             info_dict['Revision'] = str(max(kw_rev, info_dict['Revision']))
214
215         buf = ''
216         with open(os.path.join(g.wd, path), 'r') as f:
217             buf = f.read()
218
219         keywords = eval(FILES.get(path, str(kw_rev)))
220         for k in keywords:
221             for sk in svn_keywords:
222                 if k in svn_keywords[sk]:
223                     if options.clean:
224                         buf = re.sub(get_svn_keyword_re(sk), '$\\1$', buf)
225                     elif sk == 'Id':
226                         id_str = ' '.join([info_dict['Name'],
227                                            info_dict['Revision'],
228                                            info_dict['Date'],
229                                            info_dict['Author']])
230                         buf = re.sub(get_svn_keyword_re(sk), '$\\1: ' + id_str + ' $', buf)
231                     else:
232                         buf = re.sub(get_svn_keyword_re(sk), '$\\1: ' + info_dict[sk] + ' $', buf)
233
234         with open(os.path.join(g.wd, path), 'w') as f:
235             f.write(buf)
236         if options.verbose:
237             print path + ' [' + ', '.join(keywords) + '] [len: ' + str(len(buf)) +']'
238
239 if __name__ == '__main__':
240
241     parser = OptionParser(version="%prog "+str(VERSION))
242     parser.set_defaults(clean=None)
243     parser.add_option("-s", "--smudge",
244                       action="store_false", dest="clean",
245                       help="Populate svn:keywords.")
246     parser.add_option("-c", "--clean",
247                       action="store_true", dest="clean",
248                       help="Return svn:keywords to pristene state.")
249     parser.add_option("-v", "--verbose",
250                       action="store_true", dest="verbose", default=False)
251     (options, args) = parser.parse_args()
252
253     if (options.clean is None):
254         parser.print_help()
255         exit(0)
256     else:
257         try:
258             g = git.Repo()
259         except git.errors.InvalidGitRepositoryError:
260             print "You are not in a git repository or working directory."
261             exit(1)
262
263         CONFIG_PATH = os.path.join(gsk(g), 'conf.ini')
264         FILES_PATH = os.path.join(gsk(g), 'files.ini')
265         FILEINFO_PATH = os.path.join(gsk(g), 'fileinfo.ini')
266
267         CONFIG.read(CONFIG_PATH)
268         for section in ['core','CommitToRev','BlobToCommit', 'RevInfo']:
269             if not CONFIG.has_section(section):
270                 CONFIG.add_section(section)
271
272         smudge(g, options)