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

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: Reupload 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 import urlparse
15
16 import gclient_utils
17 import subcommand
18
19
20 def NormalizeUrl(url):
21 """Converts a git url to a normalized form."""
22 parsed = urlparse.urlparse(url)
23 norm_url = 'https://' + parsed.netloc + parsed.path
24 if not norm_url.endswith('.git'):
25 norm_url += '.git'
26 return norm_url
27
28
29 def UrlToCacheDir(url):
30 """Converts a git url to a normalized form for the cache dir path."""
31 parsed = urlparse.urlparse(url)
32 norm_url = parsed.netloc + parsed.path
33 if norm_url.endswith('.git'):
34 norm_url = norm_url[:-len('.git')]
35 return norm_url.replace('-', '--').replace('/', '-')
36
37
38 def RunGit(cmd, **kwargs):
39 """Runs git in a subprocess."""
40 kwargs.setdefault('cwd', os.getcwd())
41 if kwargs.get('filter_fn'):
42 kwargs['filter_fn'] = gclient_utils.GitFilter(kwargs.get('filter_fn'))
43 kwargs.setdefault('print_stdout', False)
44 env = kwargs.get('env') or kwargs.setdefault('env', os.environ.copy())
45 env.setdefault('GIT_ASKPASS', 'true')
46 env.setdefault('SSH_ASKPASS', 'true')
47 else:
48 kwargs.setdefault('print_stdout', True)
49 stdout = kwargs.get('stdout', sys.stdout)
50 print >> stdout, 'running "git %s" in "%s"' % (' '.join(cmd), kwargs['cwd'])
51 gclient_utils.CheckCallAndFilter(['git'] + cmd, **kwargs)
Ryan Tseng 2014/02/20 20:30:56 git.bat for windows, right?
agable 2014/02/21 19:33:44 Done.
52
53
54 class LockError(Exception):
55 pass
56
57
58 class Lockfile(object):
59 """Class to represent a cross-platform process-specific lockfile."""
60 def __init__(self, path):
61 self.path = os.path.abspath(path)
62 self.lockfile = self.path + ".lock"
63 self.pid = os.getpid()
64
65 def _read_pid(self):
66 """Reads the pid stored in the lockfile.
67
68 Note: This method is potentially racy. By the time it returns the lockfile
69 may have been unlocked, removed, or stolen by some other process.
70 """
71 try:
72 with open(self.lockfile, 'r') as f:
73 pid = int(f.readline().strip())
74 except (IOError, ValueError):
75 pid = None
76 return pid
77
78 def _make_lockfile(self):
79 """Safely creates a lockfile containing the current pid."""
80 open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY)
81 fd = os.open(self.lockfile, open_flags, 0o644)
82 f = os.fdopen(fd, 'w')
83 print >> f, self.pid
84 f.close()
85
86 def _remove_lockfile(self):
87 """Deletes the lockfile. Complains (implicitly) if it doesn't exist."""
88 os.remove(self.lockfile)
89
90 def lock(self):
91 """Acquire the lock.
92
93 Note: This is a NON-BLOCKING FAIL-FAST operation.
94 Do. Or do not. There is no try.
Ryan Tseng 2014/02/20 20:30:56 http://www.maniacworld.com/dog-yoda-halloween-cost
95 """
96 try:
97 self._make_lockfile()
98 except OSError as e:
99 if e.errno == errno.EEXIST:
100 raise LockError("%s is already locked" % self.path)
101 else:
102 raise LockError("Failed to create %s (err %s)" % (self.path, e.errno))
103
104 def unlock(self):
105 """Release the lock."""
106 if not self.is_locked():
107 raise LockError("%s is not locked" % self.path)
108 if not self.i_am_locking():
109 raise LockError("%s is locked, but not by me" % self.path)
110 self._remove_lockfile()
111
112 def break_lock(self):
113 """Remove the lock, even if it was created by someone else."""
114 try:
115 self._remove_lockfile()
116 return True
117 except OSError as exc:
118 if exc.errno == errno.ENOENT:
119 return False
120 else:
121 raise
122
123 def is_locked(self):
124 """Test if the file is locked by anyone.
125
126 Note: This method is potentially racy. By the time it returns the lockfile
127 may have been unlocked, removed, or stolen by some other process.
128 """
129 return os.path.exists(self.lockfile)
130
131 def i_am_locking(self):
132 """Test if the file is locked by this process."""
133 return self.is_locked() and self.pid == self._read_pid()
134
135 def __enter__(self):
136 self.lock()
137 return self
138
139 def __exit__(self, *_exc):
140 self.unlock()
141
142
143 @subcommand.usage('[url of repo to check for caching]')
144 def CMDexists(parser, args):
145 """Checks to see if there already is a cache of the given repo."""
146 options, args = parser.parse_args(args)
147 if not len(args) == 1:
148 parser.error('git cache exists only takes exactly one repo url.')
149 url = args[0]
150 repo_dir = os.path.join(options.cache_dir, UrlToCacheDir(url))
151 flag_file = os.path.join(repo_dir, 'config')
152 if os.path.isdir(repo_dir) and os.path.isfile(flag_file):
153 print repo_dir
154 return 0
155 return 1
156
157
158 @subcommand.usage('[url of repo to add to or update in cache]')
159 def CMDpopulate(parser, args):
160 """Bare clones or updates a repository in the cache."""
161 options, args = parser.parse_args(args)
Ryan Tseng 2014/02/20 20:30:56 Add a todo about a shallow clone option?
agable 2014/02/21 19:33:44 Shallow clones are done in the next patchset.
162 if not len(args) == 1:
163 parser.error('git cache populate only takes exactly one repo url.')
164 url = args[0]
165
166 gclient_utils.safe_makedirs(options.cache_dir)
167 repo_dir = os.path.join(options.cache_dir, UrlToCacheDir(url))
168
169 v = []
170 filter_fn = lambda l: '[up to date]' not in l
171 if options.verbose:
172 v = ['-v', '--progress']
173 filter_fn = None
174
175 with Lockfile(repo_dir):
176 # Setup from scratch if the repo is new or is in a bad state.
177 if not os.path.exists(os.path.join(repo_dir, 'config')):
Ryan Tseng 2014/02/20 20:30:56 Does this imply that all repos that have a "config
agable 2014/02/21 19:33:44 It implies that all repos that have a "config" fil
178 gclient_utils.rmtree(repo_dir)
179 gclient_utils.safe_makedirs(repo_dir)
180
181 RunGit(['init', '--bare'], cwd=repo_dir)
182 RunGit(['config', 'remote.origin.url', NormalizeUrl(url)],
183 cwd=repo_dir)
184 RunGit(['config', 'remote.origin.fetch',
185 '+refs/heads/*:refs/heads/*'],
Ryan Tseng 2014/02/20 20:30:56 Lets also ensure that these lines are in the confi
agable 2014/02/21 19:33:44 Also done in the next patchset -- we rewrite the c
186 cwd=repo_dir)
187 RunGit(['config', '--add', 'remote.origin.fetch',
188 '+refs/branch-heads/*:refs/branch-heads/*'],
189 cwd=repo_dir)
190 RunGit(['config', 'core.deltaBaseCacheLimit', '2g'],
191 cwd=repo_dir)
192
193 RunGit(['fetch'] + v + ['origin'],
194 filter_fn=filter_fn, cwd=repo_dir, retry=True)
195
196
197 @subcommand.usage('[url of repo to unlock, or -a|--all]')
198 def CMDunlock(parser, args):
199 """Unlocks one or all repos if their lock files are still around."""
200 parser.add_option('--force', '-f', action='store_true',
Ryan Tseng 2014/02/20 20:30:56 nit: Short options before long options
agable 2014/02/21 19:33:44 But the options.foo attribute is named after the l
201 help='actually perform the action')
Ryan Tseng 2014/02/20 20:30:56 nit: Capitalize first word, add period.
agable 2014/02/21 19:33:44 Following the example of 'git cl' and git itself h
202 parser.add_option('--all', '-a', action='store_true',
203 help='unlock all repository caches')
204 options, args = parser.parse_args(args)
205 if len(args) > 1 or (len(args) == 0 and not options.all):
206 parser.error('git cache unlock takes exactly one repo url, or --all')
207
208 if not options.all:
209 url = args[0]
210 repo_dirs = [os.path.join(options.cache_dir, UrlToCacheDir(url))]
211 else:
212 repo_dirs = [path for path in os.listdir(options.cache_dir)
213 if os.path.isdir(path)]
214 lockfiles = [repo_dir + '.lock' for repo_dir in repo_dirs
215 if os.path.exists(repo_dir + '.lock')]
216
217 if not options.force:
218 logging.warn('Not performing any actions. '
219 'Pass -f|--force to remove the following lockfiles: '
220 '%s' % lockfiles)
Ryan Tseng 2014/02/20 20:30:56 ', '.join(lockfiles) would look nicer
agable 2014/02/21 19:33:44 Done.
221 return
Ryan Tseng 2014/02/20 20:30:56 parser.error()? I can't think of any situations w
agable 2014/02/21 19:33:44 Good point, "git clean" without -f returns 128.
222
223 unlocked = untouched = []
Ryan Tseng 2014/02/20 20:30:56 I don't think this is what you want :P
agable 2014/02/21 19:33:44 Done.
224 for repo_dir in repo_dirs:
225 lf = Lockfile(repo_dir)
226 if lf.break_lock():
227 unlocked.append(repo_dir)
228 else:
229 untouched.append(repo_dir)
230
231 if unlocked:
232 logging.info('Broke locks on these caches: %s' % unlocked)
233 if untouched:
234 logging.debug('Did not touch these caches: %s' % untouched)
235
236
237 class OptionParser(optparse.OptionParser):
238 """Wrapper class for OptionParser to handle global options."""
239 def __init__(self, *args, **kwargs):
240 optparse.OptionParser.__init__(self, *args, prog='git cache', **kwargs)
241 self.add_option('-c', '--cache-dir',
242 help='Path to the directory containing the cache.')
243 self.add_option('-v', '--verbose', action='count', default=0,
244 help='Increase verbosity (can be passed multiple times).')
245
246 def parse_args(self, args=None, values=None):
247 options, args = optparse.OptionParser.parse_args(self, args, values)
248
249 try:
250 global_cache_dir = subprocess.check_output(
251 ['git', 'config', '--global', 'cache.cachepath']).strip()
Ryan Tseng 2014/02/20 20:30:56 git.bat for windows.
agable 2014/02/21 19:33:44 Done.
252 if options.cache_dir:
253 logging.warn('Overriding globally-configured cache directory.')
254 else:
255 options.cache_dir = global_cache_dir
256 except subprocess.CalledProcessError:
257 if not options.cache_dir:
258 self.error('No cache directory specified on command line '
259 'or in cache.cachepath.')
260 options.cache_dir = os.path.abspath(options.cache_dir)
261
262 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
263 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
264
265 return options, args
266
267
268 def main(argv):
269 dispatcher = subcommand.CommandDispatcher(__name__)
270 return dispatcher.execute(OptionParser(), argv)
271
272
273 if __name__ == '__main__':
274 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