Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(17)

Side by Side Diff: git_cache.py

Issue 164823002: Create "git cache" command. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Created 6 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « git-cache ('k') | tests/gclient_utils_test.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 # Copyright 2014 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 """A git command for managing a local cache of git repositories."""
7
8 import errno
9 import logging
10 import optparse
11 import os
12 import subprocess
13 import sys
14
15 import gclient_utils
16 import subcommand
17
18
19 def DieWithError(message):
20 print >> sys.stderr, message
21 sys.exit(1)
22
23
24 def UrlToCacheDir(url):
25 """Converts a git url to a normalized form for the cache dir path."""
26 idx = url.find('://')
hinoka 2014/02/13 23:01:29 szager mentioned this in a different CL, but lets
agable 2014/02/14 00:28:31 Done.
27 if idx != -1:
28 url = url[idx+3:]
29 if not url.endswith('.git'):
30 url += '.git'
31 return url.replace('-', '--').replace('/', '-')
32
33
34 def RunGit(cmd, **kwargs):
35 """Runs git in a subprocess."""
36 kwargs.setdefault('cwd', os.getcwd)
hinoka 2014/02/13 23:01:29 os.getcwd()
agable 2014/02/14 00:28:31 Done.
37 if kwargs.get('filter_fn'):
38 kwargs['filter_fn'] = gclient_utils.GitFilter(kwargs.get('filter_fn'))
39 kwargs.setdefault('print_stdout', False)
40 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
41 env.setdefault('GIT_ASKPASS', 'true')
42 env.setdefault('SSH_ASKPASS', 'true')
43 else:
44 kwargs.setdefault('print_stdout', True)
45 stdout = kwargs.get('stdout', sys.stdout)
46 stdout.write(
hinoka 2014/02/13 23:01:29 print >>stdout, "..." ? \n usage might be flaky on
agable 2014/02/19 02:25:51 Done.
47 'running \'git %s\' in \'%s\'\n' % (' '.join(cmd), kwargs['cwd']))
48 gclient_utils.CheckCallAndFilter(['git'] + cmd, **kwargs)
49
50
51 class LockError(Exception):
52 pass
53
54
55 class Lockfile(object):
56 """Class to represent a cross-platform process-specific lockfile."""
57 def __init__(self, path):
58 self.path = os.path.abspath(path)
59 self.lockfile = self.path + ".lock"
60 self.pid = os.getpid()
61
62 def _read_pid(self):
iannucci 2014/02/14 01:43:08 This method only really makes sense when stealing.
agable 2014/02/19 02:25:51 I seriously considered having _read_pid() and stea
63 """Reads the pid stored in the lockfile."""
64 try:
65 with open(self.lockfile, 'r') as f:
66 pid = int(f.readline().strip())
67 except (IOError, ValueError):
68 pid = None
69 return pid
70
71 def _make_lockfile(self):
72 """Safely creates a lockfile containing the current pid."""
73 open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
74 open_mode = 0o644
iannucci 2014/02/14 01:43:08 Not sure what the value in having this be a extra
agable 2014/02/19 02:25:51 Done.
75 fd = os.open(self.lockfile, open_flags, open_mode)
76 f = os.fdopen(fd, 'w')
hinoka 2014/02/13 23:01:29 with os.fdopen(fd, 'w') as f: ?
agable 2014/02/14 00:28:31 AFAIK only builtin open() is a context manager, os
iannucci 2014/02/14 01:43:08 and also print >> f, self.pid :)
agable 2014/02/19 02:25:51 Done.
77 f.write('%s\n' % self.pid)
78 f.close()
79
80 def _remove_lockfile(self):
81 """Deletes the lockfile. Does nothing if it doesn't exist."""
82 try:
83 os.remove(self.lockfile)
84 except OSError as exc:
85 if exc.errno == errno.ENOENT:
86 pass
87 else:
88 raise
89
90 def lock(self):
91 """Acquire the lock."""
92 try:
93 self._make_lockfile()
94 except OSError as e:
95 if e.errno == errno.EEXIST:
96 raise LockError("%s is already locked" % self.path)
97 else:
98 raise LockError("failed to create %s" % self.path)
hinoka 2014/02/13 23:01:29 s/failed/Failed/ Also might as well include e.errn
agable 2014/02/14 00:28:31 Done.
99
100 def unlock(self):
101 """Release the lock."""
102 if not self.is_locked():
103 raise LockError("%s is not locked" % self.path)
104 if not self.i_am_locking():
105 raise LockError("%s is locked, but not by me" % self.path)
106 self._remove_lockfile()
107
108 def steal(self):
hinoka 2014/02/13 23:01:29 This isn't used anywhere? Need a CMDwipeoutallthel
agable 2014/02/14 00:28:31 Heh. Yeah, adding CMDmineallmine() in this patchse
109 """Break a lock and replace it with one of our own.
110
111 Note that this is potentially racy (someone else could put a lockfile in
112 place between breaking the lock and placing our own). This is fine,
113 since we only want to use this method when it is completely safe.
114 """
115 old_pid = self._read_pid()
116 if old_pid:
117 print 'Breaking lock on %s left behind by process %d' % (
118 self.path, old_pid)
119 self._remove_lockfile()
120 self._make_lockfile()
121
122 def is_locked(self):
123 """Test if the file is locked by anyone."""
iannucci 2014/02/14 01:43:08 note, this is racy... P1: lock P2: is_locked? P1:
agable 2014/02/19 02:25:51 I think you mean "yes" and "ask me again later" :P
124 return os.path.exists(self.lockfile)
125
126 def i_am_locking(self):
127 """Test if the file is locked by this process."""
128 return self.is_locked() and self.pid == self._read_pid()
iannucci 2014/02/14 01:43:08 This should just be True iff make_lockfile didn't
agable 2014/02/19 02:25:51 ...and no one else has stolen the lock. Checking t
129
130 def __enter__(self):
131 self.lock()
132 return self
133
134 def __exit__(self, *_exc):
135 self.unlock()
136
137
138 @subcommand.usage('[url of repo to check for caching]')
139 def CMDexists(parser, args):
140 """Checks to see if there already is a cache of the given repo."""
hinoka 2014/02/13 23:01:29 Whats the purpose of this? It looks like it retur
agable 2014/02/14 00:28:31 Its purpose is to test for existence, and print th
iannucci 2014/02/14 01:43:08 I disagree a bit. I think: 0 exists, 1 DNE, -1 bad
agable 2014/02/19 02:25:51 parser.error() automatically generates retcode 2 f
141 options, args = parser.parse_args(args)
142 if not len(args) == 1:
143 DieWithError('git cache exists only takes exactly one repo url.')
hinoka 2014/02/13 23:01:29 lets use parser.error() instead
agable 2014/02/14 00:28:31 Done.
144 url = args[0]
145 repo_dir = os.path.join(options.cache_dir, UrlToCacheDir(url))
146 flag_file = os.path.join(repo_dir, 'config')
147 if os.path.isdir(repo_dir) and os.path.isfile(flag_file):
148 print repo_dir
149 return 0
150 return 1
151
152
153 @subcommand.usage('[url of repo to add to or update in cache]')
154 def CMDpopulate(parser, args):
155 """Bare clones or updates a repository in the cache."""
156 parser.add_option('--local',
157 help='local repository to initialize from')
hinoka 2014/02/13 23:01:29 nit: align at paren. Only align at 4 spaces if fi
agable 2014/02/14 00:28:31 Done.
iannucci 2014/02/14 01:43:08 should we auto-default to the current repo, if the
agable 2014/02/19 02:25:51 What does git clone do if --reference points to an
158 options, args = parser.parse_args(args)
159 if not len(args) == 1:
160 DieWithError('git cache populate only takes exactly one repo url.')
hinoka 2014/02/13 23:01:29 lets use parser.error() instead
agable 2014/02/14 00:28:31 Done.
161 url = args[0]
162
163 gclient_utils.safe_makedirs(options.cache_dir)
164 repo_dir = os.path.join(options.cache_dir, UrlToCacheDir(url))
165
166 # If we've been supplied with a local repository to help out,
167 # make sure that it is a full direct clone before relying on it.
168 local_objects = local_altfile = ''
169 if options.local:
170 local_objects = os.path.join(
171 os.path.abspath(options.local), '.git', 'objects')
172 local_altfile = os.path.join(local_objects, 'info', 'alternates')
173 use_reference = (
174 os.path.exists(local_objects) and not os.path.exists(local_altfile))
175 altfile = os.path.join(repo_dir, 'objects', 'info', 'alternates')
176
177 v = []
178 filter_fn = lambda l: '[up to date]' not in l
179 if options.verbose:
180 v = ['-v']
181 filter_fn = None
182
183 with Lockfile(repo_dir):
hinoka 2014/02/13 23:01:29 "with Lock" generally means "wait for this lock",
agable 2014/02/14 00:28:31 Yeah, we already talked about this, but replying h
iannucci 2014/02/14 01:43:08 Racy
agable 2014/02/19 02:25:51 Yep.
184 # Do a full clone if the repo is new or is in a bad state.
185 if not os.path.exists(os.path.join(repo_dir, 'config')):
186 gclient_utils.rmtree(repo_dir)
187 cmd = ['clone'] + v + ['-c', 'core.deltaBaseCacheLimit=2g',
188 '--progress', '--bare']
189
190 if use_reference:
191 cmd += ['--reference', os.path.abspath(options.local)]
192
193 RunGit(cmd + [url, repo_dir],
194 filter_fn=filter_fn, cwd=options.cache_dir, retry=True)
195
196 else:
197 if use_reference:
198 with open(altfile, 'w') as f:
199 f.write(os.path.abspath(local_objects))
200
201 RunGit(['fetch'] + v + ['--multiple', '--progress', '--all'],
202 filter_fn=filter_fn, cwd=repo_dir, retry=True)
203
204 # If the clone has an object dependency on the local repo, break it
205 # with repack and remove the linkage.
iannucci 2014/02/14 01:43:08 may consider an optimization: * if the local rep
agable 2014/02/19 02:25:51 I think not worth it. In addition, repack -ad is p
206 if os.path.exists(altfile):
207 RunGit(['repack', '-a'], cwd=repo_dir)
208 os.remove(altfile)
209
210
211 class OptionParser(optparse.OptionParser):
212 """Wrapper class for OptionParser to handle global options."""
213 def __init__(self, *args, **kwargs):
214 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
215 self.add_option('-c', '--cache-dir',
216 help='Path to the directory containing the cache.')
217 self.add_option('-v', '--verbose', action='count', default=0,
218 help='Increase verbosity (can be passed multiple times).')
219
220 def parse_args(self, args=None, values=None):
221 options, args = optparse.OptionParser.parse_args(self, args, values)
222
223 try:
224 global_cache_dir = subprocess.check_output(
225 ['git', 'config', '--global', 'cache.cachepath']).strip()
226 if options.cache_dir:
hinoka 2014/02/13 23:01:29 The if/else doesn't need to be in the try block.
agable 2014/02/14 00:28:31 Well, it kinda does. I can move it out but then I
227 logging.warn('Overriding globally-configured cache directory.')
228 else:
229 options.cache_dir = global_cache_dir
230 except subprocess.CalledProcessError:
231 if not options.cache_dir:
232 DieWithError('No cache directory specified on command line '
hinoka 2014/02/13 23:01:29 lets use parser.error() instead
agable 2014/02/14 00:28:31 Done.
233 'or in cache.cachepath.')
234 options.cache_dir = os.path.abspath(options.cache_dir)
235
236 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
237 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
238
239 return options, args
240
241
242 def main(argv):
243 dispatcher = subcommand.CommandDispatcher(__name__)
244 return dispatcher.execute(OptionParser(), argv)
245
246
247 if __name__ == '__main__':
248 sys.exit(main(sys.argv[1:]))
OLDNEW
« no previous file with comments | « git-cache ('k') | tests/gclient_utils_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698