##// END OF EJS Templates
help: add/fix docstrings for a bunch of extensions
Dirkjan Ochtman -
r8873:e872ef2e default
parent child Browse files
Show More
@@ -1,130 +1,131 b''
1 1 # perf.py - performance test routines
2 '''helper extension to measure performance'''
2 3
3 4 from mercurial import cmdutil, match, commands
4 5 import time, os, sys
5 6
6 7 def timer(func):
7 8 results = []
8 9 begin = time.time()
9 10 count = 0
10 11 while 1:
11 12 ostart = os.times()
12 13 cstart = time.time()
13 14 r = func()
14 15 cstop = time.time()
15 16 ostop = os.times()
16 17 count += 1
17 18 a, b = ostart, ostop
18 19 results.append((cstop - cstart, b[0] - a[0], b[1]-a[1]))
19 20 if cstop - begin > 3 and count >= 100:
20 21 break
21 22 if cstop - begin > 10 and count >= 3:
22 23 break
23 24 if r:
24 25 sys.stderr.write("! result: %s\n" % r)
25 26 m = min(results)
26 27 sys.stderr.write("! wall %f comb %f user %f sys %f (best of %d)\n"
27 28 % (m[0], m[1] + m[2], m[1], m[2], count))
28 29
29 30 def perfwalk(ui, repo, *pats):
30 31 try:
31 32 m = cmdutil.match(repo, pats, {})
32 33 timer(lambda: len(list(repo.dirstate.walk(m, True, False))))
33 34 except:
34 35 try:
35 36 m = cmdutil.match(repo, pats, {})
36 37 timer(lambda: len([b for a,b,c in repo.dirstate.statwalk([], m)]))
37 38 except:
38 39 timer(lambda: len(list(cmdutil.walk(repo, pats, {}))))
39 40
40 41 def perfstatus(ui, repo, *pats):
41 42 #m = match.always(repo.root, repo.getcwd())
42 43 #timer(lambda: sum(map(len, repo.dirstate.status(m, False, False, False))))
43 44 timer(lambda: sum(map(len, repo.status())))
44 45
45 46 def perfheads(ui, repo):
46 47 timer(lambda: len(repo.changelog.heads()))
47 48
48 49 def perftags(ui, repo):
49 50 import mercurial.changelog, mercurial.manifest
50 51 def t():
51 52 repo.changelog = mercurial.changelog.changelog(repo.sopener)
52 53 repo.manifest = mercurial.manifest.manifest(repo.sopener)
53 54 repo.tagscache = None
54 55 return len(repo.tags())
55 56 timer(t)
56 57
57 58 def perfdirstate(ui, repo):
58 59 "a" in repo.dirstate
59 60 def d():
60 61 repo.dirstate.invalidate()
61 62 "a" in repo.dirstate
62 63 timer(d)
63 64
64 65 def perfdirstatedirs(ui, repo):
65 66 "a" in repo.dirstate
66 67 def d():
67 68 "a" in repo.dirstate._dirs
68 69 del repo.dirstate._dirs
69 70 timer(d)
70 71
71 72 def perfmanifest(ui, repo):
72 73 def d():
73 74 t = repo.manifest.tip()
74 75 m = repo.manifest.read(t)
75 76 repo.manifest.mapcache = None
76 77 repo.manifest._cache = None
77 78 timer(d)
78 79
79 80 def perfindex(ui, repo):
80 81 import mercurial.changelog
81 82 def d():
82 83 t = repo.changelog.tip()
83 84 repo.changelog = mercurial.changelog.changelog(repo.sopener)
84 85 repo.changelog._loadindexmap()
85 86 timer(d)
86 87
87 88 def perfstartup(ui, repo):
88 89 cmd = sys.argv[0]
89 90 def d():
90 91 os.system("HGRCPATH= %s version -q > /dev/null" % cmd)
91 92 timer(d)
92 93
93 94 def perfparents(ui, repo):
94 95 nl = [repo.changelog.node(i) for i in xrange(1000)]
95 96 def d():
96 97 for n in nl:
97 98 repo.changelog.parents(n)
98 99 timer(d)
99 100
100 101 def perflookup(ui, repo, rev):
101 102 timer(lambda: len(repo.lookup(rev)))
102 103
103 104 def perflog(ui, repo):
104 105 ui.pushbuffer()
105 106 timer(lambda: commands.log(ui, repo, rev=[], date='', user=''))
106 107 ui.popbuffer()
107 108
108 109 def perftemplating(ui, repo):
109 110 ui.pushbuffer()
110 111 timer(lambda: commands.log(ui, repo, rev=[], date='', user='',
111 112 template='{date|shortdate} [{rev}:{node|short}]'
112 113 ' {author|person}: {desc|firstline}\n'))
113 114 ui.popbuffer()
114 115
115 116 cmdtable = {
116 117 'perflookup': (perflookup, []),
117 118 'perfparents': (perfparents, []),
118 119 'perfstartup': (perfstartup, []),
119 120 'perfstatus': (perfstatus, []),
120 121 'perfwalk': (perfwalk, []),
121 122 'perfmanifest': (perfmanifest, []),
122 123 'perfindex': (perfindex, []),
123 124 'perfheads': (perfheads, []),
124 125 'perftags': (perftags, []),
125 126 'perfdirstate': (perfdirstate, []),
126 127 'perfdirstatedirs': (perfdirstate, []),
127 128 'perflog': (perflog, []),
128 129 'perftemplating': (perftemplating, []),
129 130 }
130 131
@@ -1,99 +1,99 b''
1 1 # acl.py - changeset access control for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2, incorporated herein by reference.
7 7 #
8 # this hook allows to allow or deny access to parts of a repo when
9 # taking incoming changesets.
10 #
11 # authorization is against local user name on system where hook is
12 # run, not committer of original changeset (since that is easy to
13 # spoof).
14 #
15 # acl hook is best to use if you use hgsh to set up restricted shells
16 # for authenticated users to only push to / pull from. not safe if
17 # user has interactive shell access, because they can disable hook.
18 # also not safe if remote users share one local account, because then
19 # no way to tell remote users apart.
20 #
21 # to use, configure acl extension in hgrc like this:
22 #
23 # [extensions]
24 # hgext.acl =
25 #
26 # [hooks]
27 # pretxnchangegroup.acl = python:hgext.acl.hook
28 #
29 # [acl]
30 # sources = serve # check if source of incoming changes in this list
31 # # ("serve" == ssh or http, "push", "pull", "bundle")
32 #
33 # allow and deny lists have subtree pattern (default syntax is glob)
34 # on left, user names on right. deny list checked before allow list.
35 #
36 # [acl.allow]
37 # # if acl.allow not present, all users allowed by default
38 # # empty acl.allow = no users allowed
39 # docs/** = doc_writer
40 # .hgtags = release_engineer
41 #
42 # [acl.deny]
43 # # if acl.deny not present, no users denied by default
44 # # empty acl.deny = all users allowed
45 # glob pattern = user4, user5
46 # ** = user6
8
9 '''provide simple hooks for access control
10
11 Authorization is against local user name on system where hook is run, not
12 committer of original changeset (since that is easy to spoof).
13
14 The acl hook is best to use if you use hgsh to set up restricted shells for
15 authenticated users to only push to / pull from. It's not safe if user has
16 interactive shell access, because they can disable the hook. It's also not
17 safe if remote users share one local account, because then there's no way to
18 tell remote users apart.
19
20 To use, configure the acl extension in hgrc like this:
21
22 [extensions]
23 hgext.acl =
24
25 [hooks]
26 pretxnchangegroup.acl = python:hgext.acl.hook
27
28 [acl]
29 sources = serve # check if source of incoming changes in this list
30 # ("serve" == ssh or http, "push", "pull", "bundle")
31
32 Allow and deny lists have a subtree pattern (default syntax is glob) on the
33 left and user names on right. The deny list is checked before the allow list.
34
35 [acl.allow]
36 # if acl.allow not present, all users allowed by default
37 # empty acl.allow = no users allowed
38 docs/** = doc_writer
39 .hgtags = release_engineer
40
41 [acl.deny]
42 # if acl.deny not present, no users denied by default
43 # empty acl.deny = all users allowed
44 glob pattern = user4, user5
45 ** = user6
46 '''
47 47
48 48 from mercurial.i18n import _
49 49 from mercurial import util, match
50 50 import getpass, urllib
51 51
52 52 def buildmatch(ui, repo, user, key):
53 53 '''return tuple of (match function, list enabled).'''
54 54 if not ui.has_section(key):
55 55 ui.debug(_('acl: %s not enabled\n') % key)
56 56 return None
57 57
58 58 pats = [pat for pat, users in ui.configitems(key)
59 59 if user in users.replace(',', ' ').split()]
60 60 ui.debug(_('acl: %s enabled, %d entries for user %s\n') %
61 61 (key, len(pats), user))
62 62 if pats:
63 63 return match.match(repo.root, '', pats)
64 64 return match.exact(repo.root, '', [])
65 65
66 66
67 67 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
68 68 if hooktype != 'pretxnchangegroup':
69 69 raise util.Abort(_('config error - hook type "%s" cannot stop '
70 70 'incoming changesets') % hooktype)
71 71 if source not in ui.config('acl', 'sources', 'serve').split():
72 72 ui.debug(_('acl: changes have source "%s" - skipping\n') % source)
73 73 return
74 74
75 75 user = None
76 76 if source == 'serve' and 'url' in kwargs:
77 77 url = kwargs['url'].split(':')
78 78 if url[0] == 'remote' and url[1].startswith('http'):
79 79 user = urllib.unquote(url[2])
80 80
81 81 if user is None:
82 82 user = getpass.getuser()
83 83
84 84 cfg = ui.config('acl', 'config')
85 85 if cfg:
86 86 ui.readconfig(cfg, sections = ['acl.allow', 'acl.deny'])
87 87 allow = buildmatch(ui, repo, user, 'acl.allow')
88 88 deny = buildmatch(ui, repo, user, 'acl.deny')
89 89
90 90 for rev in xrange(repo[node], len(repo)):
91 91 ctx = repo[rev]
92 92 for f in ctx.files():
93 93 if deny and deny(f):
94 94 ui.debug(_('acl: user %s denied on %s\n') % (user, f))
95 95 raise util.Abort(_('acl: access denied for changeset %s') % ctx)
96 96 if allow and not allow(f):
97 97 ui.debug(_('acl: user %s not allowed on %s\n') % (user, f))
98 98 raise util.Abort(_('acl: access denied for changeset %s') % ctx)
99 99 ui.debug(_('acl: allowing changeset %s\n') % ctx)
@@ -1,42 +1,44 b''
1 1 # Mercurial extension to provide the 'hg children' command
2 2 #
3 3 # Copyright 2007 by Intevation GmbH <intevation@intevation.de>
4 4 #
5 5 # Author(s):
6 6 # Thomas Arendsen Hein <thomas@intevation.de>
7 7 #
8 8 # This software may be used and distributed according to the terms of the
9 9 # GNU General Public License version 2, incorporated herein by reference.
10 10
11 '''provides children command to show children changesets'''
12
11 13 from mercurial import cmdutil
12 14 from mercurial.commands import templateopts
13 15 from mercurial.i18n import _
14 16
15 17
16 18 def children(ui, repo, file_=None, **opts):
17 19 """show the children of the given or working directory revision
18 20
19 21 Print the children of the working directory's revisions. If a
20 22 revision is given via -r/--rev, the children of that revision will
21 23 be printed. If a file argument is given, revision in which the
22 24 file was last changed (after the working directory revision or the
23 25 argument to --rev if given) is printed.
24 26 """
25 27 rev = opts.get('rev')
26 28 if file_:
27 29 ctx = repo.filectx(file_, changeid=rev)
28 30 else:
29 31 ctx = repo[rev]
30 32
31 33 displayer = cmdutil.show_changeset(ui, repo, opts)
32 34 for cctx in ctx.children():
33 35 displayer.show(cctx)
34 36
35 37
36 38 cmdtable = {
37 39 "children":
38 40 (children,
39 41 [('r', 'rev', '', _('show children of the specified revision')),
40 42 ] + templateopts,
41 43 _('hg children [-r REV] [FILE]')),
42 44 }
@@ -1,228 +1,229 b''
1 1 # extdiff.py - external diff program support for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2, incorporated herein by reference.
7 7
8 '''
8 '''allow external programs to compare revisions
9
9 10 The `extdiff' Mercurial extension allows you to use external programs
10 11 to compare revisions, or revision with working directory. The external diff
11 12 programs are called with a configurable set of options and two
12 13 non-option arguments: paths to directories containing snapshots of
13 14 files to compare.
14 15
15 16 The `extdiff' extension also allows to configure new diff commands, so
16 17 you do not need to type "hg extdiff -p kdiff3" always.
17 18
18 19 [extdiff]
19 20 # add new command that runs GNU diff(1) in 'context diff' mode
20 21 cdiff = gdiff -Nprc5
21 22 ## or the old way:
22 23 #cmd.cdiff = gdiff
23 24 #opts.cdiff = -Nprc5
24 25
25 26 # add new command called vdiff, runs kdiff3
26 27 vdiff = kdiff3
27 28
28 29 # add new command called meld, runs meld (no need to name twice)
29 30 meld =
30 31
31 32 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
32 33 # (see http://www.vim.org/scripts/script.php?script_id=102)
33 34 # Non English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
34 35 # your .vimrc
35 36 vimdiff = gvim -f '+next' '+execute "DirDiff" argv(0) argv(1)'
36 37
37 38 You can use -I/-X and list of file or directory names like normal "hg
38 39 diff" command. The `extdiff' extension makes snapshots of only needed
39 40 files, so running the external diff program will actually be pretty
40 41 fast (at least faster than having to compare the entire tree).
41 42 '''
42 43
43 44 from mercurial.i18n import _
44 45 from mercurial.node import short
45 46 from mercurial import cmdutil, util, commands
46 47 import os, shlex, shutil, tempfile
47 48
48 49 def snapshot(ui, repo, files, node, tmproot):
49 50 '''snapshot files as of some revision
50 51 if not using snapshot, -I/-X does not work and recursive diff
51 52 in tools like kdiff3 and meld displays too many files.'''
52 53 dirname = os.path.basename(repo.root)
53 54 if dirname == "":
54 55 dirname = "root"
55 56 if node is not None:
56 57 dirname = '%s.%s' % (dirname, short(node))
57 58 base = os.path.join(tmproot, dirname)
58 59 os.mkdir(base)
59 60 if node is not None:
60 61 ui.note(_('making snapshot of %d files from rev %s\n') %
61 62 (len(files), short(node)))
62 63 else:
63 64 ui.note(_('making snapshot of %d files from working directory\n') %
64 65 (len(files)))
65 66 wopener = util.opener(base)
66 67 fns_and_mtime = []
67 68 ctx = repo[node]
68 69 for fn in files:
69 70 wfn = util.pconvert(fn)
70 71 if not wfn in ctx:
71 72 # skipping new file after a merge ?
72 73 continue
73 74 ui.note(' %s\n' % wfn)
74 75 dest = os.path.join(base, wfn)
75 76 fctx = ctx[wfn]
76 77 data = repo.wwritedata(wfn, fctx.data())
77 78 if 'l' in fctx.flags():
78 79 wopener.symlink(data, wfn)
79 80 else:
80 81 wopener(wfn, 'w').write(data)
81 82 if 'x' in fctx.flags():
82 83 util.set_flags(dest, False, True)
83 84 if node is None:
84 85 fns_and_mtime.append((dest, repo.wjoin(fn), os.path.getmtime(dest)))
85 86 return dirname, fns_and_mtime
86 87
87 88 def dodiff(ui, repo, diffcmd, diffopts, pats, opts):
88 89 '''Do the actuall diff:
89 90
90 91 - copy to a temp structure if diffing 2 internal revisions
91 92 - copy to a temp structure if diffing working revision with
92 93 another one and more than 1 file is changed
93 94 - just invoke the diff for a single file in the working dir
94 95 '''
95 96
96 97 revs = opts.get('rev')
97 98 change = opts.get('change')
98 99
99 100 if revs and change:
100 101 msg = _('cannot specify --rev and --change at the same time')
101 102 raise util.Abort(msg)
102 103 elif change:
103 104 node2 = repo.lookup(change)
104 105 node1 = repo[node2].parents()[0].node()
105 106 else:
106 107 node1, node2 = cmdutil.revpair(repo, revs)
107 108
108 109 matcher = cmdutil.match(repo, pats, opts)
109 110 modified, added, removed = repo.status(node1, node2, matcher)[:3]
110 111 if not (modified or added or removed):
111 112 return 0
112 113
113 114 tmproot = tempfile.mkdtemp(prefix='extdiff.')
114 115 dir2root = ''
115 116 try:
116 117 # Always make a copy of node1
117 118 dir1 = snapshot(ui, repo, modified + removed, node1, tmproot)[0]
118 119 changes = len(modified) + len(removed) + len(added)
119 120
120 121 # If node2 in not the wc or there is >1 change, copy it
121 122 if node2 or changes > 1:
122 123 dir2, fns_and_mtime = snapshot(ui, repo, modified + added, node2, tmproot)
123 124 else:
124 125 # This lets the diff tool open the changed file directly
125 126 dir2 = ''
126 127 dir2root = repo.root
127 128 fns_and_mtime = []
128 129
129 130 # If only one change, diff the files instead of the directories
130 131 if changes == 1 :
131 132 if len(modified):
132 133 dir1 = os.path.join(dir1, util.localpath(modified[0]))
133 134 dir2 = os.path.join(dir2root, dir2, util.localpath(modified[0]))
134 135 elif len(removed) :
135 136 dir1 = os.path.join(dir1, util.localpath(removed[0]))
136 137 dir2 = os.devnull
137 138 else:
138 139 dir1 = os.devnull
139 140 dir2 = os.path.join(dir2root, dir2, util.localpath(added[0]))
140 141
141 142 cmdline = ('%s %s %s %s' %
142 143 (util.shellquote(diffcmd), ' '.join(diffopts),
143 144 util.shellquote(dir1), util.shellquote(dir2)))
144 145 ui.debug(_('running %r in %s\n') % (cmdline, tmproot))
145 146 util.system(cmdline, cwd=tmproot)
146 147
147 148 for copy_fn, working_fn, mtime in fns_and_mtime:
148 149 if os.path.getmtime(copy_fn) != mtime:
149 150 ui.debug(_('file changed while diffing. '
150 151 'Overwriting: %s (src: %s)\n') % (working_fn, copy_fn))
151 152 util.copyfile(copy_fn, working_fn)
152 153
153 154 return 1
154 155 finally:
155 156 ui.note(_('cleaning up temp directory\n'))
156 157 shutil.rmtree(tmproot)
157 158
158 159 def extdiff(ui, repo, *pats, **opts):
159 160 '''use external program to diff repository (or selected files)
160 161
161 162 Show differences between revisions for the specified files, using
162 163 an external program. The default program used is diff, with
163 164 default options "-Npru".
164 165
165 166 To select a different program, use the -p/--program option. The
166 167 program will be passed the names of two directories to compare. To
167 168 pass additional options to the program, use -o/--option. These
168 169 will be passed before the names of the directories to compare.
169 170
170 171 When two revision arguments are given, then changes are shown
171 172 between those revisions. If only one revision is specified then
172 173 that revision is compared to the working directory, and, when no
173 174 revisions are specified, the working directory files are compared
174 175 to its parent.'''
175 176 program = opts['program'] or 'diff'
176 177 if opts['program']:
177 178 option = opts['option']
178 179 else:
179 180 option = opts['option'] or ['-Npru']
180 181 return dodiff(ui, repo, program, option, pats, opts)
181 182
182 183 cmdtable = {
183 184 "extdiff":
184 185 (extdiff,
185 186 [('p', 'program', '', _('comparison program to run')),
186 187 ('o', 'option', [], _('pass option to comparison program')),
187 188 ('r', 'rev', [], _('revision')),
188 189 ('c', 'change', '', _('change made by revision')),
189 190 ] + commands.walkopts,
190 191 _('hg extdiff [OPT]... [FILE]...')),
191 192 }
192 193
193 194 def uisetup(ui):
194 195 for cmd, path in ui.configitems('extdiff'):
195 196 if cmd.startswith('cmd.'):
196 197 cmd = cmd[4:]
197 198 if not path: path = cmd
198 199 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
199 200 diffopts = diffopts and [diffopts] or []
200 201 elif cmd.startswith('opts.'):
201 202 continue
202 203 else:
203 204 # command = path opts
204 205 if path:
205 206 diffopts = shlex.split(path)
206 207 path = diffopts.pop(0)
207 208 else:
208 209 path, diffopts = cmd, []
209 210 def save(cmd, path, diffopts):
210 211 '''use closure to save diff command to use'''
211 212 def mydiff(ui, repo, *pats, **opts):
212 213 return dodiff(ui, repo, path, diffopts, pats, opts)
213 214 mydiff.__doc__ = '''use %(path)s to diff repository (or selected files)
214 215
215 216 Show differences between revisions for the specified
216 217 files, using the %(path)s program.
217 218
218 219 When two revision arguments are given, then changes are
219 220 shown between those revisions. If only one revision is
220 221 specified then that revision is compared to the working
221 222 directory, and, when no revisions are specified, the
222 223 working directory files are compared to its parent.''' % {
223 224 'path': util.uirepr(path),
224 225 }
225 226 return mydiff
226 227 cmdtable[cmd] = (save(cmd, path, diffopts),
227 228 cmdtable['extdiff'][1][1:],
228 229 _('hg %s [OPTION]... [FILE]...') % cmd)
@@ -1,283 +1,283 b''
1 # GnuPG signing extension for Mercurial
2 #
3 1 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
4 2 #
5 3 # This software may be used and distributed according to the terms of the
6 4 # GNU General Public License version 2, incorporated herein by reference.
7 5
6 '''GnuPG signing extension for Mercurial'''
7
8 8 import os, tempfile, binascii
9 9 from mercurial import util, commands, match
10 10 from mercurial import node as hgnode
11 11 from mercurial.i18n import _
12 12
13 13 class gpg(object):
14 14 def __init__(self, path, key=None):
15 15 self.path = path
16 16 self.key = (key and " --local-user \"%s\"" % key) or ""
17 17
18 18 def sign(self, data):
19 19 gpgcmd = "%s --sign --detach-sign%s" % (self.path, self.key)
20 20 return util.filter(data, gpgcmd)
21 21
22 22 def verify(self, data, sig):
23 23 """ returns of the good and bad signatures"""
24 24 sigfile = datafile = None
25 25 try:
26 26 # create temporary files
27 27 fd, sigfile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".sig")
28 28 fp = os.fdopen(fd, 'wb')
29 29 fp.write(sig)
30 30 fp.close()
31 31 fd, datafile = tempfile.mkstemp(prefix="hg-gpg-", suffix=".txt")
32 32 fp = os.fdopen(fd, 'wb')
33 33 fp.write(data)
34 34 fp.close()
35 35 gpgcmd = ("%s --logger-fd 1 --status-fd 1 --verify "
36 36 "\"%s\" \"%s\"" % (self.path, sigfile, datafile))
37 37 ret = util.filter("", gpgcmd)
38 38 finally:
39 39 for f in (sigfile, datafile):
40 40 try:
41 41 if f: os.unlink(f)
42 42 except: pass
43 43 keys = []
44 44 key, fingerprint = None, None
45 45 err = ""
46 46 for l in ret.splitlines():
47 47 # see DETAILS in the gnupg documentation
48 48 # filter the logger output
49 49 if not l.startswith("[GNUPG:]"):
50 50 continue
51 51 l = l[9:]
52 52 if l.startswith("ERRSIG"):
53 53 err = _("error while verifying signature")
54 54 break
55 55 elif l.startswith("VALIDSIG"):
56 56 # fingerprint of the primary key
57 57 fingerprint = l.split()[10]
58 58 elif (l.startswith("GOODSIG") or
59 59 l.startswith("EXPSIG") or
60 60 l.startswith("EXPKEYSIG") or
61 61 l.startswith("BADSIG")):
62 62 if key is not None:
63 63 keys.append(key + [fingerprint])
64 64 key = l.split(" ", 2)
65 65 fingerprint = None
66 66 if err:
67 67 return err, []
68 68 if key is not None:
69 69 keys.append(key + [fingerprint])
70 70 return err, keys
71 71
72 72 def newgpg(ui, **opts):
73 73 """create a new gpg instance"""
74 74 gpgpath = ui.config("gpg", "cmd", "gpg")
75 75 gpgkey = opts.get('key')
76 76 if not gpgkey:
77 77 gpgkey = ui.config("gpg", "key", None)
78 78 return gpg(gpgpath, gpgkey)
79 79
80 80 def sigwalk(repo):
81 81 """
82 82 walk over every sigs, yields a couple
83 83 ((node, version, sig), (filename, linenumber))
84 84 """
85 85 def parsefile(fileiter, context):
86 86 ln = 1
87 87 for l in fileiter:
88 88 if not l:
89 89 continue
90 90 yield (l.split(" ", 2), (context, ln))
91 91 ln +=1
92 92
93 93 # read the heads
94 94 fl = repo.file(".hgsigs")
95 95 for r in reversed(fl.heads()):
96 96 fn = ".hgsigs|%s" % hgnode.short(r)
97 97 for item in parsefile(fl.read(r).splitlines(), fn):
98 98 yield item
99 99 try:
100 100 # read local signatures
101 101 fn = "localsigs"
102 102 for item in parsefile(repo.opener(fn), fn):
103 103 yield item
104 104 except IOError:
105 105 pass
106 106
107 107 def getkeys(ui, repo, mygpg, sigdata, context):
108 108 """get the keys who signed a data"""
109 109 fn, ln = context
110 110 node, version, sig = sigdata
111 111 prefix = "%s:%d" % (fn, ln)
112 112 node = hgnode.bin(node)
113 113
114 114 data = node2txt(repo, node, version)
115 115 sig = binascii.a2b_base64(sig)
116 116 err, keys = mygpg.verify(data, sig)
117 117 if err:
118 118 ui.warn("%s:%d %s\n" % (fn, ln , err))
119 119 return None
120 120
121 121 validkeys = []
122 122 # warn for expired key and/or sigs
123 123 for key in keys:
124 124 if key[0] == "BADSIG":
125 125 ui.write(_("%s Bad signature from \"%s\"\n") % (prefix, key[2]))
126 126 continue
127 127 if key[0] == "EXPSIG":
128 128 ui.write(_("%s Note: Signature has expired"
129 129 " (signed by: \"%s\")\n") % (prefix, key[2]))
130 130 elif key[0] == "EXPKEYSIG":
131 131 ui.write(_("%s Note: This key has expired"
132 132 " (signed by: \"%s\")\n") % (prefix, key[2]))
133 133 validkeys.append((key[1], key[2], key[3]))
134 134 return validkeys
135 135
136 136 def sigs(ui, repo):
137 137 """list signed changesets"""
138 138 mygpg = newgpg(ui)
139 139 revs = {}
140 140
141 141 for data, context in sigwalk(repo):
142 142 node, version, sig = data
143 143 fn, ln = context
144 144 try:
145 145 n = repo.lookup(node)
146 146 except KeyError:
147 147 ui.warn(_("%s:%d node does not exist\n") % (fn, ln))
148 148 continue
149 149 r = repo.changelog.rev(n)
150 150 keys = getkeys(ui, repo, mygpg, data, context)
151 151 if not keys:
152 152 continue
153 153 revs.setdefault(r, [])
154 154 revs[r].extend(keys)
155 155 for rev in sorted(revs, reverse=True):
156 156 for k in revs[rev]:
157 157 r = "%5d:%s" % (rev, hgnode.hex(repo.changelog.node(rev)))
158 158 ui.write("%-30s %s\n" % (keystr(ui, k), r))
159 159
160 160 def check(ui, repo, rev):
161 161 """verify all the signatures there may be for a particular revision"""
162 162 mygpg = newgpg(ui)
163 163 rev = repo.lookup(rev)
164 164 hexrev = hgnode.hex(rev)
165 165 keys = []
166 166
167 167 for data, context in sigwalk(repo):
168 168 node, version, sig = data
169 169 if node == hexrev:
170 170 k = getkeys(ui, repo, mygpg, data, context)
171 171 if k:
172 172 keys.extend(k)
173 173
174 174 if not keys:
175 175 ui.write(_("No valid signature for %s\n") % hgnode.short(rev))
176 176 return
177 177
178 178 # print summary
179 179 ui.write("%s is signed by:\n" % hgnode.short(rev))
180 180 for key in keys:
181 181 ui.write(" %s\n" % keystr(ui, key))
182 182
183 183 def keystr(ui, key):
184 184 """associate a string to a key (username, comment)"""
185 185 keyid, user, fingerprint = key
186 186 comment = ui.config("gpg", fingerprint, None)
187 187 if comment:
188 188 return "%s (%s)" % (user, comment)
189 189 else:
190 190 return user
191 191
192 192 def sign(ui, repo, *revs, **opts):
193 193 """add a signature for the current or given revision
194 194
195 195 If no revision is given, the parent of the working directory is used,
196 196 or tip if no revision is checked out.
197 197
198 198 See 'hg help dates' for a list of formats valid for -d/--date.
199 199 """
200 200
201 201 mygpg = newgpg(ui, **opts)
202 202 sigver = "0"
203 203 sigmessage = ""
204 204
205 205 date = opts.get('date')
206 206 if date:
207 207 opts['date'] = util.parsedate(date)
208 208
209 209 if revs:
210 210 nodes = [repo.lookup(n) for n in revs]
211 211 else:
212 212 nodes = [node for node in repo.dirstate.parents()
213 213 if node != hgnode.nullid]
214 214 if len(nodes) > 1:
215 215 raise util.Abort(_('uncommitted merge - please provide a '
216 216 'specific revision'))
217 217 if not nodes:
218 218 nodes = [repo.changelog.tip()]
219 219
220 220 for n in nodes:
221 221 hexnode = hgnode.hex(n)
222 222 ui.write("Signing %d:%s\n" % (repo.changelog.rev(n),
223 223 hgnode.short(n)))
224 224 # build data
225 225 data = node2txt(repo, n, sigver)
226 226 sig = mygpg.sign(data)
227 227 if not sig:
228 228 raise util.Abort(_("Error while signing"))
229 229 sig = binascii.b2a_base64(sig)
230 230 sig = sig.replace("\n", "")
231 231 sigmessage += "%s %s %s\n" % (hexnode, sigver, sig)
232 232
233 233 # write it
234 234 if opts['local']:
235 235 repo.opener("localsigs", "ab").write(sigmessage)
236 236 return
237 237
238 238 for x in repo.status(unknown=True)[:5]:
239 239 if ".hgsigs" in x and not opts["force"]:
240 240 raise util.Abort(_("working copy of .hgsigs is changed "
241 241 "(please commit .hgsigs manually "
242 242 "or use --force)"))
243 243
244 244 repo.wfile(".hgsigs", "ab").write(sigmessage)
245 245
246 246 if '.hgsigs' not in repo.dirstate:
247 247 repo.add([".hgsigs"])
248 248
249 249 if opts["no_commit"]:
250 250 return
251 251
252 252 message = opts['message']
253 253 if not message:
254 254 message = "\n".join([_("Added signature for changeset %s")
255 255 % hgnode.short(n)
256 256 for n in nodes])
257 257 try:
258 258 m = match.exact(repo.root, '', ['.hgsigs'])
259 259 repo.commit(message, opts['user'], opts['date'], match=m)
260 260 except ValueError, inst:
261 261 raise util.Abort(str(inst))
262 262
263 263 def node2txt(repo, node, ver):
264 264 """map a manifest into some text"""
265 265 if ver == "0":
266 266 return "%s\n" % hgnode.hex(node)
267 267 else:
268 268 raise util.Abort(_("unknown signature version"))
269 269
270 270 cmdtable = {
271 271 "sign":
272 272 (sign,
273 273 [('l', 'local', None, _('make the signature local')),
274 274 ('f', 'force', None, _('sign even if the sigfile is modified')),
275 275 ('', 'no-commit', None, _('do not commit the sigfile after signing')),
276 276 ('k', 'key', '', _('the key id to sign with')),
277 277 ('m', 'message', '', _('commit message')),
278 278 ] + commands.commitopts2,
279 279 _('hg sign [OPTION]... [REVISION]...')),
280 280 "sigcheck": (check, [], _('hg sigcheck REVISION')),
281 281 "sigs": (sigs, [], _('hg sigs')),
282 282 }
283 283
@@ -1,97 +1,96 b''
1 1 # Mercurial extension to make it easy to refer to the parent of a revision
2 2 #
3 3 # Copyright (C) 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2, incorporated herein by reference.
7 7
8 '''\
9 use suffixes to refer to ancestor revisions
8 '''use suffixes to refer to ancestor revisions
10 9
11 10 This extension allows you to use git-style suffixes to refer to the
12 11 ancestors of a specific revision.
13 12
14 13 For example, if you can refer to a revision as "foo", then:
15 14
16 15 - foo^N = Nth parent of foo
17 16 foo^0 = foo
18 17 foo^1 = first parent of foo
19 18 foo^2 = second parent of foo
20 19 foo^ = foo^1
21 20
22 21 - foo~N = Nth first grandparent of foo
23 22 foo~0 = foo
24 23 foo~1 = foo^1 = foo^ = first parent of foo
25 24 foo~2 = foo^1^1 = foo^^ = first parent of first parent of foo
26 25 '''
27 26 from mercurial import error
28 27
29 28 def reposetup(ui, repo):
30 29 if not repo.local():
31 30 return
32 31
33 32 class parentrevspecrepo(repo.__class__):
34 33 def lookup(self, key):
35 34 try:
36 35 _super = super(parentrevspecrepo, self)
37 36 return _super.lookup(key)
38 37 except error.RepoError:
39 38 pass
40 39
41 40 circ = key.find('^')
42 41 tilde = key.find('~')
43 42 if circ < 0 and tilde < 0:
44 43 raise
45 44 elif circ >= 0 and tilde >= 0:
46 45 end = min(circ, tilde)
47 46 else:
48 47 end = max(circ, tilde)
49 48
50 49 cl = self.changelog
51 50 base = key[:end]
52 51 try:
53 52 node = _super.lookup(base)
54 53 except error.RepoError:
55 54 # eek - reraise the first error
56 55 return _super.lookup(key)
57 56
58 57 rev = cl.rev(node)
59 58 suffix = key[end:]
60 59 i = 0
61 60 while i < len(suffix):
62 61 # foo^N => Nth parent of foo
63 62 # foo^0 == foo
64 63 # foo^1 == foo^ == 1st parent of foo
65 64 # foo^2 == 2nd parent of foo
66 65 if suffix[i] == '^':
67 66 j = i + 1
68 67 p = cl.parentrevs(rev)
69 68 if j < len(suffix) and suffix[j].isdigit():
70 69 j += 1
71 70 n = int(suffix[i+1:j])
72 71 if n > 2 or n == 2 and p[1] == -1:
73 72 raise
74 73 else:
75 74 n = 1
76 75 if n:
77 76 rev = p[n - 1]
78 77 i = j
79 78 # foo~N => Nth first grandparent of foo
80 79 # foo~0 = foo
81 80 # foo~1 = foo^1 == foo^ == 1st parent of foo
82 81 # foo~2 = foo^1^1 == foo^^ == 1st parent of 1st parent of foo
83 82 elif suffix[i] == '~':
84 83 j = i + 1
85 84 while j < len(suffix) and suffix[j].isdigit():
86 85 j += 1
87 86 if j == i + 1:
88 87 raise
89 88 n = int(suffix[i+1:j])
90 89 for k in xrange(n):
91 90 rev = cl.parentrevs(rev)[0]
92 91 i = j
93 92 else:
94 93 raise
95 94 return cl.node(rev)
96 95
97 96 repo.__class__ = parentrevspecrepo
@@ -1,106 +1,108 b''
1 1 # Copyright (C) 2006 - Marco Barisione <marco@barisione.org>
2 2 #
3 3 # This is a small extension for Mercurial (http://www.selenic.com/mercurial)
4 4 # that removes files not known to mercurial
5 5 #
6 6 # This program was inspired by the "cvspurge" script contained in CVS utilities
7 7 # (http://www.red-bean.com/cvsutils/).
8 8 #
9 9 # For help on the usage of "hg purge" use:
10 10 # hg help purge
11 11 #
12 12 # This program is free software; you can redistribute it and/or modify
13 13 # it under the terms of the GNU General Public License as published by
14 14 # the Free Software Foundation; either version 2 of the License, or
15 15 # (at your option) any later version.
16 16 #
17 17 # This program is distributed in the hope that it will be useful,
18 18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 20 # GNU General Public License for more details.
21 21 #
22 22 # You should have received a copy of the GNU General Public License
23 23 # along with this program; if not, write to the Free Software
24 24 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25 25
26 '''enable removing untracked files only'''
27
26 28 from mercurial import util, commands, cmdutil
27 29 from mercurial.i18n import _
28 30 import os, stat
29 31
30 32 def purge(ui, repo, *dirs, **opts):
31 33 '''removes files not tracked by Mercurial
32 34
33 35 Delete files not known to Mercurial. This is useful to test local
34 36 and uncommitted changes in an otherwise-clean source tree.
35 37
36 38 This means that purge will delete:
37 39 - Unknown files: files marked with "?" by "hg status"
38 40 - Empty directories: in fact Mercurial ignores directories unless
39 41 they contain files under source control management
40 42 But it will leave untouched:
41 43 - Modified and unmodified tracked files
42 44 - Ignored files (unless --all is specified)
43 45 - New files added to the repository (with "hg add")
44 46
45 47 If directories are given on the command line, only files in these
46 48 directories are considered.
47 49
48 50 Be careful with purge, as you could irreversibly delete some files
49 51 you forgot to add to the repository. If you only want to print the
50 52 list of files that this program would delete, use the --print
51 53 option.
52 54 '''
53 55 act = not opts['print']
54 56 eol = '\n'
55 57 if opts['print0']:
56 58 eol = '\0'
57 59 act = False # --print0 implies --print
58 60
59 61 def remove(remove_func, name):
60 62 if act:
61 63 try:
62 64 remove_func(repo.wjoin(name))
63 65 except OSError:
64 66 m = _('%s cannot be removed') % name
65 67 if opts['abort_on_err']:
66 68 raise util.Abort(m)
67 69 ui.warn(_('warning: %s\n') % m)
68 70 else:
69 71 ui.write('%s%s' % (name, eol))
70 72
71 73 def removefile(path):
72 74 try:
73 75 os.remove(path)
74 76 except OSError:
75 77 # read-only files cannot be unlinked under Windows
76 78 s = os.stat(path)
77 79 if (s.st_mode & stat.S_IWRITE) != 0:
78 80 raise
79 81 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
80 82 os.remove(path)
81 83
82 84 directories = []
83 85 match = cmdutil.match(repo, dirs, opts)
84 86 match.dir = directories.append
85 87 status = repo.status(match=match, ignored=opts['all'], unknown=True)
86 88
87 89 for f in sorted(status[4] + status[5]):
88 90 ui.note(_('Removing file %s\n') % f)
89 91 remove(removefile, f)
90 92
91 93 for f in sorted(directories, reverse=True):
92 94 if match(f) and not os.listdir(repo.wjoin(f)):
93 95 ui.note(_('Removing directory %s\n') % f)
94 96 remove(os.rmdir, f)
95 97
96 98 cmdtable = {
97 99 'purge|clean':
98 100 (purge,
99 101 [('a', 'abort-on-err', None, _('abort if an error occurs')),
100 102 ('', 'all', None, _('purge ignored files too')),
101 103 ('p', 'print', None, _('print filenames instead of deleting them')),
102 104 ('0', 'print0', None, _('end filenames with NUL, for use with xargs'
103 105 ' (implies -p/--print)')),
104 106 ] + commands.walkopts,
105 107 _('hg purge [OPTION]... [DIR]...'))
106 108 }
@@ -1,31 +1,31 b''
1 # Mercurial extension to provide the 'hg share' command
2 #
3 1 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 2 #
5 3 # This software may be used and distributed according to the terms of the
6 4 # GNU General Public License version 2, incorporated herein by reference.
7 5
6 '''provides the hg share command'''
7
8 8 import os
9 9 from mercurial.i18n import _
10 10 from mercurial import hg, commands
11 11
12 12 def share(ui, source, dest=None, noupdate=False):
13 13 """create a new shared repository (experimental)
14 14
15 15 Initialize a new repository and working directory that shares its
16 16 history with another repository.
17 17
18 18 NOTE: actions that change history such as rollback or moving the
19 19 source may confuse sharers.
20 20 """
21 21
22 22 return hg.share(ui, source, dest, not noupdate)
23 23
24 24 cmdtable = {
25 25 "share":
26 26 (share,
27 27 [('U', 'noupdate', None, _('do not create a working copy'))],
28 28 _('[-U] SOURCE [DEST]')),
29 29 }
30 30
31 31 commands.norepo += " share"
@@ -1,155 +1,158 b''
1 1 # win32text.py - LF <-> CRLF/CR translation utilities for Windows/Mac users
2 2 #
3 3 # Copyright 2005, 2007-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2, incorporated herein by reference.
7 #
8 # To perform automatic newline conversion, use:
9 #
10 # [extensions]
11 # hgext.win32text =
12 # [encode]
13 # ** = cleverencode:
14 # # or ** = macencode:
15 # [decode]
16 # ** = cleverdecode:
17 # # or ** = macdecode:
18 #
19 # If not doing conversion, to make sure you do not commit CRLF/CR by
20 # accident:
21 #
22 # [hooks]
23 # pretxncommit.crlf = python:hgext.win32text.forbidcrlf
24 # # or pretxncommit.cr = python:hgext.win32text.forbidcr
25 #
26 # To do the same check on a server to prevent CRLF/CR from being
27 # pushed or pulled:
28 #
29 # [hooks]
30 # pretxnchangegroup.crlf = python:hgext.win32text.forbidcrlf
31 # # or pretxnchangegroup.cr = python:hgext.win32text.forbidcr
7
8 '''LF <-> CRLF/CR translation utilities
9
10 To perform automatic newline conversion, use:
11
12 [extensions]
13 hgext.win32text =
14 [encode]
15 ** = cleverencode:
16 # or ** = macencode:
17
18 [decode]
19 ** = cleverdecode:
20 # or ** = macdecode:
21
22 If not doing conversion, to make sure you do not commit CRLF/CR by accident:
23
24 [hooks]
25 pretxncommit.crlf = python:hgext.win32text.forbidcrlf
26 # or pretxncommit.cr = python:hgext.win32text.forbidcr
27
28 To do the same check on a server to prevent CRLF/CR from being
29 pushed or pulled:
30
31 [hooks]
32 pretxnchangegroup.crlf = python:hgext.win32text.forbidcrlf
33 # or pretxnchangegroup.cr = python:hgext.win32text.forbidcr
34 '''
32 35
33 36 from mercurial.i18n import _
34 37 from mercurial.node import short
35 38 from mercurial import util
36 39 import re
37 40
38 41 # regexp for single LF without CR preceding.
39 42 re_single_lf = re.compile('(^|[^\r])\n', re.MULTILINE)
40 43
41 44 newlinestr = {'\r\n': 'CRLF', '\r': 'CR'}
42 45 filterstr = {'\r\n': 'clever', '\r': 'mac'}
43 46
44 47 def checknewline(s, newline, ui=None, repo=None, filename=None):
45 48 # warn if already has 'newline' in repository.
46 49 # it might cause unexpected eol conversion.
47 50 # see issue 302:
48 51 # http://www.selenic.com/mercurial/bts/issue302
49 52 if newline in s and ui and filename and repo:
50 53 ui.warn(_('WARNING: %s already has %s line endings\n'
51 54 'and does not need EOL conversion by the win32text plugin.\n'
52 55 'Before your next commit, please reconsider your '
53 56 'encode/decode settings in \nMercurial.ini or %s.\n') %
54 57 (filename, newlinestr[newline], repo.join('hgrc')))
55 58
56 59 def dumbdecode(s, cmd, **kwargs):
57 60 checknewline(s, '\r\n', **kwargs)
58 61 # replace single LF to CRLF
59 62 return re_single_lf.sub('\\1\r\n', s)
60 63
61 64 def dumbencode(s, cmd):
62 65 return s.replace('\r\n', '\n')
63 66
64 67 def macdumbdecode(s, cmd, **kwargs):
65 68 checknewline(s, '\r', **kwargs)
66 69 return s.replace('\n', '\r')
67 70
68 71 def macdumbencode(s, cmd):
69 72 return s.replace('\r', '\n')
70 73
71 74 def cleverdecode(s, cmd, **kwargs):
72 75 if not util.binary(s):
73 76 return dumbdecode(s, cmd, **kwargs)
74 77 return s
75 78
76 79 def cleverencode(s, cmd):
77 80 if not util.binary(s):
78 81 return dumbencode(s, cmd)
79 82 return s
80 83
81 84 def macdecode(s, cmd, **kwargs):
82 85 if not util.binary(s):
83 86 return macdumbdecode(s, cmd, **kwargs)
84 87 return s
85 88
86 89 def macencode(s, cmd):
87 90 if not util.binary(s):
88 91 return macdumbencode(s, cmd)
89 92 return s
90 93
91 94 _filters = {
92 95 'dumbdecode:': dumbdecode,
93 96 'dumbencode:': dumbencode,
94 97 'cleverdecode:': cleverdecode,
95 98 'cleverencode:': cleverencode,
96 99 'macdumbdecode:': macdumbdecode,
97 100 'macdumbencode:': macdumbencode,
98 101 'macdecode:': macdecode,
99 102 'macencode:': macencode,
100 103 }
101 104
102 105 def forbidnewline(ui, repo, hooktype, node, newline, **kwargs):
103 106 halt = False
104 107 seen = set()
105 108 # we try to walk changesets in reverse order from newest to
106 109 # oldest, so that if we see a file multiple times, we take the
107 110 # newest version as canonical. this prevents us from blocking a
108 111 # changegroup that contains an unacceptable commit followed later
109 112 # by a commit that fixes the problem.
110 113 tip = repo['tip']
111 114 for rev in xrange(len(repo)-1, repo[node].rev()-1, -1):
112 115 c = repo[rev]
113 116 for f in c.files():
114 117 if f in seen or f not in tip or f not in c:
115 118 continue
116 119 seen.add(f)
117 120 data = c[f].data()
118 121 if not util.binary(data) and newline in data:
119 122 if not halt:
120 123 ui.warn(_('Attempt to commit or push text file(s) '
121 124 'using %s line endings\n') %
122 125 newlinestr[newline])
123 126 ui.warn(_('in %s: %s\n') % (short(c.node()), f))
124 127 halt = True
125 128 if halt and hooktype == 'pretxnchangegroup':
126 129 crlf = newlinestr[newline].lower()
127 130 filter = filterstr[newline]
128 131 ui.warn(_('\nTo prevent this mistake in your local repository,\n'
129 132 'add to Mercurial.ini or .hg/hgrc:\n'
130 133 '\n'
131 134 '[hooks]\n'
132 135 'pretxncommit.%s = python:hgext.win32text.forbid%s\n'
133 136 '\n'
134 137 'and also consider adding:\n'
135 138 '\n'
136 139 '[extensions]\n'
137 140 'hgext.win32text =\n'
138 141 '[encode]\n'
139 142 '** = %sencode:\n'
140 143 '[decode]\n'
141 144 '** = %sdecode:\n') % (crlf, crlf, filter, filter))
142 145 return halt
143 146
144 147 def forbidcrlf(ui, repo, hooktype, node, **kwargs):
145 148 return forbidnewline(ui, repo, hooktype, node, '\r\n', **kwargs)
146 149
147 150 def forbidcr(ui, repo, hooktype, node, **kwargs):
148 151 return forbidnewline(ui, repo, hooktype, node, '\r', **kwargs)
149 152
150 153 def reposetup(ui, repo):
151 154 if not repo.local():
152 155 return
153 156 for name, fn in _filters.iteritems():
154 157 repo.adddatafilter(name, fn)
155 158
General Comments 0
You need to be logged in to leave comments. Login now