1
-#!/usr/bin/env python
2
-
3
-"""Functionality for interacting with Git repositories.
4
-
5
-This module provides classes for interfacing with a Git repository.
6
-"""
7
-
8
-import os
9
-import re
10
-import time
11
-from binascii import hexlify
12
-from cStringIO import StringIO
13
-import unittest
14
-
15
-from git_remote_helpers.util import debug, error, die, start_command, run_command
16
-
17
-
18
-def get_git_dir ():
19
- """Return the path to the GIT_DIR for this repo."""
20
- args = ("git", "rev-parse", "--git-dir")
21
- exit_code, output, errors = run_command(args)
22
- if exit_code:
23
- die("Failed to retrieve git dir")
24
- assert not errors
25
- return output.strip()
26
-
27
-
28
-def parse_git_config ():
29
- """Return a dict containing the parsed version of 'git config -l'."""
30
- exit_code, output, errors = run_command(("git", "config", "-z", "-l"))
31
- if exit_code:
32
- die("Failed to retrieve git configuration")
33
- assert not errors
34
- return dict([e.split('\n', 1) for e in output.split("\0") if e])
35
-
36
-
37
-def git_config_bool (value):
38
- """Convert the given git config string value to True or False.
39
-
40
- Raise ValueError if the given string was not recognized as a
41
- boolean value.
42
-
43
- """
44
- norm_value = str(value).strip().lower()
45
- if norm_value in ("true", "1", "yes", "on", ""):
46
- return True
47
- if norm_value in ("false", "0", "no", "off", "none"):
48
- return False
49
- raise ValueError("Failed to parse '%s' into a boolean value" % (value))
50
-
51
-
52
-def valid_git_ref (ref_name):
53
- """Return True iff the given ref name is a valid git ref name."""
54
- # The following is a reimplementation of the git check-ref-format
55
- # command. The rules were derived from the git check-ref-format(1)
56
- # manual page. This code should be replaced by a call to
57
- # check_refname_format() in the git library, when such is available.
58
- if ref_name.endswith('/') or \
59
- ref_name.startswith('.') or \
60
- ref_name.count('/.') or \
61
- ref_name.count('..') or \
62
- ref_name.endswith('.lock'):
63
- return False
64
- for c in ref_name:
65
- if ord(c) < 0x20 or ord(c) == 0x7f or c in " ~^:?*[":
66
- return False
67
- return True
68
-
69
-
70
-class GitObjectFetcher(object):
71
-
72
- """Provide parsed access to 'git cat-file --batch'.
73
-
74
- This provides a read-only interface to the Git object database.
75
-
76
- """
77
-
78
- def __init__ (self):
79
- """Initiate a 'git cat-file --batch' session."""
80
- self.queue = [] # List of object names to be submitted
81
- self.in_transit = None # Object name currently in transit
82
-
83
- # 'git cat-file --batch' produces binary output which is likely
84
- # to be corrupted by the default "rU"-mode pipe opened by
85
- # start_command. (Mode == "rU" does universal new-line
86
- # conversion, which mangles carriage returns.) Therefore, we
87
- # open an explicitly binary-safe pipe for transferring the
88
- # output from 'git cat-file --batch'.
89
- pipe_r_fd, pipe_w_fd = os.pipe()
90
- pipe_r = os.fdopen(pipe_r_fd, "rb")
91
- pipe_w = os.fdopen(pipe_w_fd, "wb")
92
- self.proc = start_command(("git", "cat-file", "--batch"),
93
- stdout = pipe_w)
94
- self.f = pipe_r
95
-
96
- def __del__ (self):
97
- """Verify completed communication with 'git cat-file --batch'."""
98
- assert not self.queue
99
- assert self.in_transit is None
100
- self.proc.stdin.close()
101
- assert self.proc.wait() == 0 # Zero exit code
102
- assert self.f.read() == "" # No remaining output
103
-
104
- def _submit_next_object (self):
105
- """Submit queue items to the 'git cat-file --batch' process.
106
-
107
- If there are items in the queue, and there is currently no item
108
- currently in 'transit', then pop the first item off the queue,
109
- and submit it.
110
-
111
- """
112
- if self.queue and self.in_transit is None:
113
- self.in_transit = self.queue.pop(0)
114
- print >> self.proc.stdin, self.in_transit[0]
115
-
116
- def push (self, obj, callback):
117
- """Push the given object name onto the queue.
118
-
119
- The given callback function will at some point in the future
120
- be called exactly once with the following arguments:
121
- - self - this GitObjectFetcher instance
122
- - obj - the object name provided to push()
123
- - sha1 - the SHA1 of the object, if 'None' obj is missing
124
- - t - the type of the object (tag/commit/tree/blob)
125
- - size - the size of the object in bytes
126
- - data - the object contents
127
-
128
- """
129
- self.queue.append((obj, callback))
130
- self._submit_next_object() # (Re)start queue processing
131
-
132
- def process_next_entry (self):
133
- """Read the next entry off the queue and invoke callback."""
134
- obj, cb = self.in_transit
135
- self.in_transit = None
136
- header = self.f.readline()
137
- if header == "%s missing\n" % (obj):
138
- cb(self, obj, None, None, None, None)
139
- return
140
- sha1, t, size = header.split(" ")
141
- assert len(sha1) == 40
142
- assert t in ("tag", "commit", "tree", "blob")
143
- assert size.endswith("\n")
144
- size = int(size.strip())
145
- data = self.f.read(size)
146
- assert self.f.read(1) == "\n"
147
- cb(self, obj, sha1, t, size, data)
148
- self._submit_next_object()
149
-
150
- def process (self):
151
- """Process the current queue until empty."""
152
- while self.in_transit is not None:
153
- self.process_next_entry()
154
-
155
- # High-level convenience methods:
156
-
157
- def get_sha1 (self, objspec):
158
- """Return the SHA1 of the object specified by 'objspec'.
159
-
160
- Return None if 'objspec' does not specify an existing object.
161
-
162
- """
163
- class _ObjHandler(object):
164
- """Helper class for getting the returned SHA1."""
165
- def __init__ (self, parser):
166
- self.parser = parser
167
- self.sha1 = None
168
-
169
- def __call__ (self, parser, obj, sha1, t, size, data):
170
- # FIXME: Many unused arguments. Could this be cheaper?
171
- assert parser == self.parser
172
- self.sha1 = sha1
173
-
174
- handler = _ObjHandler(self)
175
- self.push(objspec, handler)
176
- self.process()
177
- return handler.sha1
178
-
179
- def open_obj (self, objspec):
180
- """Return a file object wrapping the contents of a named object.
181
-
182
- The caller is responsible for calling .close() on the returned
183
- file object.
184
-
185
- Raise KeyError if 'objspec' does not exist in the repo.
186
-
187
- """
188
- class _ObjHandler(object):
189
- """Helper class for parsing the returned git object."""
190
- def __init__ (self, parser):
191
- """Set up helper."""
192
- self.parser = parser
193
- self.contents = StringIO()
194
- self.err = None
195
-
196
- def __call__ (self, parser, obj, sha1, t, size, data):
197
- """Git object callback (see GitObjectFetcher documentation)."""
198
- assert parser == self.parser
199
- if not sha1: # Missing object
200
- self.err = "Missing object '%s'" % obj
201
- else:
202
- assert size == len(data)
203
- self.contents.write(data)
204
-
205
- handler = _ObjHandler(self)
206
- self.push(objspec, handler)
207
- self.process()
208
- if handler.err:
209
- raise KeyError(handler.err)
210
- handler.contents.seek(0)
211
- return handler.contents
212
-
213
- def walk_tree (self, tree_objspec, callback, prefix = ""):
214
- """Recursively walk the given Git tree object.
215
-
216
- Recursively walk all subtrees of the given tree object, and
217
- invoke the given callback passing three arguments:
218
- (path, mode, data) with the path, permission bits, and contents
219
- of all the blobs found in the entire tree structure.
220
-
221
- """
222
- class _ObjHandler(object):
223
- """Helper class for walking a git tree structure."""
224
- def __init__ (self, parser, cb, path, mode = None):
225
- """Set up helper."""
226
- self.parser = parser
227
- self.cb = cb
228
- self.path = path
229
- self.mode = mode
230
- self.err = None
231
-
232
- def parse_tree (self, treedata):
233
- """Parse tree object data, yield tree entries.
234
-
235
- Each tree entry is a 3-tuple (mode, sha1, path)
236
-
237
- self.path is prepended to all paths yielded
238
- from this method.
239
-
240
- """
241
- while treedata:
242
- mode = int(treedata[:6], 10)
243
- # Turn 100xxx into xxx
244
- if mode > 100000:
245
- mode -= 100000
246
- assert treedata[6] == " "
247
- i = treedata.find("\0", 7)
248
- assert i > 0
249
- path = treedata[7:i]
250
- sha1 = hexlify(treedata[i + 1: i + 21])
251
- yield (mode, sha1, self.path + path)
252
- treedata = treedata[i + 21:]
253
-
254
- def __call__ (self, parser, obj, sha1, t, size, data):
255
- """Git object callback (see GitObjectFetcher documentation)."""
256
- assert parser == self.parser
257
- if not sha1: # Missing object
258
- self.err = "Missing object '%s'" % (obj)
259
- return
260
- assert size == len(data)
261
- if t == "tree":
262
- if self.path:
263
- self.path += "/"
264
- # Recurse into all blobs and subtrees
265
- for m, s, p in self.parse_tree(data):
266
- parser.push(s,
267
- self.__class__(self.parser, self.cb, p, m))
268
- elif t == "blob":
269
- self.cb(self.path, self.mode, data)
270
- else:
271
- raise ValueError("Unknown object type '%s'" % (t))
272
-
273
- self.push(tree_objspec, _ObjHandler(self, callback, prefix))
274
- self.process()
275
-
276
-
277
-class GitRefMap(object):
278
-
279
- """Map Git ref names to the Git object names they currently point to.
280
-
281
- Behaves like a dictionary of Git ref names -> Git object names.
282
-
283
- """
284
-
285
- def __init__ (self, obj_fetcher):
286
- """Create a new Git ref -> object map."""
287
- self.obj_fetcher = obj_fetcher
288
- self._cache = {} # dict: refname -> objname
289
-
290
- def _load (self, ref):
291
- """Retrieve the object currently bound to the given ref.
292
-
293
- The name of the object pointed to by the given ref is stored
294
- into this mapping, and also returned.
295
-
296
- """
297
- if ref not in self._cache:
298
- self._cache[ref] = self.obj_fetcher.get_sha1(ref)
299
- return self._cache[ref]
300
-
301
- def __contains__ (self, refname):
302
- """Return True if the given refname is present in this cache."""
303
- return bool(self._load(refname))
304
-
305
- def __getitem__ (self, refname):
306
- """Return the git object name pointed to by the given refname."""
307
- commit = self._load(refname)
308
- if commit is None:
309
- raise KeyError("Unknown ref '%s'" % (refname))
310
- return commit
311
-
312
- def get (self, refname, default = None):
313
- """Return the git object name pointed to by the given refname."""
314
- commit = self._load(refname)
315
- if commit is None:
316
- return default
317
- return commit
318
-
319
-
320
-class GitFICommit(object):
321
-
322
- """Encapsulate the data in a Git fast-import commit command."""
323
-
324
- SHA1RE = re.compile(r'^[0-9a-f]{40}$')
325
-
326
- @classmethod
327
- def parse_mode (cls, mode):
328
- """Verify the given git file mode, and return it as a string."""
329
- assert mode in (644, 755, 100644, 100755, 120000)
330
- return "%i" % (mode)
331
-
332
- @classmethod
333
- def parse_objname (cls, objname):
334
- """Return the given object name (or mark number) as a string."""
335
- if isinstance(objname, int): # Object name is a mark number
336
- assert objname > 0
337
- return ":%i" % (objname)
338
-
339
- # No existence check is done, only checks for valid format
340
- assert cls.SHA1RE.match(objname) # Object name is valid SHA1
341
- return objname
342
-
343
- @classmethod
344
- def quote_path (cls, path):
345
- """Return a quoted version of the given path."""
346
- path = path.replace("\\", "\\\\")
347
- path = path.replace("\n", "\\n")
348
- path = path.replace('"', '\\"')
349
- return '"%s"' % (path)
350
-
351
- @classmethod
352
- def parse_path (cls, path):
353
- """Verify that the given path is valid, and quote it, if needed."""
354
- assert not isinstance(path, int) # Cannot be a mark number
355
-
356
- # These checks verify the rules on the fast-import man page
357
- assert not path.count("//")
358
- assert not path.endswith("/")
359
- assert not path.startswith("/")
360
- assert not path.count("/./")
361
- assert not path.count("/../")
362
- assert not path.endswith("/.")
363
- assert not path.endswith("/..")
364
- assert not path.startswith("./")
365
- assert not path.startswith("../")
366
-
367
- if path.count('"') + path.count('\n') + path.count('\\'):
368
- return cls.quote_path(path)
369
- return path
370
-
371
- def __init__ (self, name, email, timestamp, timezone, message):
372
- """Create a new Git fast-import commit, with the given metadata."""
373
- self.name = name
374
- self.email = email
375
- self.timestamp = timestamp
376
- self.timezone = timezone
377
- self.message = message
378
- self.pathops = [] # List of path operations in this commit
379
-
380
- def modify (self, mode, blobname, path):
381
- """Add a file modification to this Git fast-import commit."""
382
- self.pathops.append(("M",
383
- self.parse_mode(mode),
384
- self.parse_objname(blobname),
385
- self.parse_path(path)))
386
-
387
- def delete (self, path):
388
- """Add a file deletion to this Git fast-import commit."""
389
- self.pathops.append(("D", self.parse_path(path)))
390
-
391
- def copy (self, path, newpath):
392
- """Add a file copy to this Git fast-import commit."""
393
- self.pathops.append(("C",
394
- self.parse_path(path),
395
- self.parse_path(newpath)))
396
-
397
- def rename (self, path, newpath):
398
- """Add a file rename to this Git fast-import commit."""
399
- self.pathops.append(("R",
400
- self.parse_path(path),
401
- self.parse_path(newpath)))
402
-
403
- def note (self, blobname, commit):
404
- """Add a note object to this Git fast-import commit."""
405
- self.pathops.append(("N",
406
- self.parse_objname(blobname),
407
- self.parse_objname(commit)))
408
-
409
- def deleteall (self):
410
- """Delete all files in this Git fast-import commit."""
411
- self.pathops.append("deleteall")
412
-
413
-
414
-class TestGitFICommit(unittest.TestCase):
415
-
416
- """GitFICommit selftests."""
417
-
418
- def test_basic (self):
419
- """GitFICommit basic selftests."""
420
-
421
- def expect_fail (method, data):
422
- """Verify that the method(data) raises an AssertionError."""
423
- try:
424
- method(data)
425
- except AssertionError:
426
- return
427
- raise AssertionError("Failed test for invalid data '%s(%s)'" %
428
- (method.__name__, repr(data)))
429
-
430
- def test_parse_mode (self):
431
- """GitFICommit.parse_mode() selftests."""
432
- self.assertEqual(GitFICommit.parse_mode(644), "644")
433
- self.assertEqual(GitFICommit.parse_mode(755), "755")
434
- self.assertEqual(GitFICommit.parse_mode(100644), "100644")
435
- self.assertEqual(GitFICommit.parse_mode(100755), "100755")
436
- self.assertEqual(GitFICommit.parse_mode(120000), "120000")
437
- self.assertRaises(AssertionError, GitFICommit.parse_mode, 0)
438
- self.assertRaises(AssertionError, GitFICommit.parse_mode, 123)
439
- self.assertRaises(AssertionError, GitFICommit.parse_mode, 600)
440
- self.assertRaises(AssertionError, GitFICommit.parse_mode, "644")
441
- self.assertRaises(AssertionError, GitFICommit.parse_mode, "abc")
442
-
443
- def test_parse_objname (self):
444
- """GitFICommit.parse_objname() selftests."""
445
- self.assertEqual(GitFICommit.parse_objname(1), ":1")
446
- self.assertRaises(AssertionError, GitFICommit.parse_objname, 0)
447
- self.assertRaises(AssertionError, GitFICommit.parse_objname, -1)
448
- self.assertEqual(GitFICommit.parse_objname("0123456789" * 4),
449
- "0123456789" * 4)
450
- self.assertEqual(GitFICommit.parse_objname("2468abcdef" * 4),
451
- "2468abcdef" * 4)
452
- self.assertRaises(AssertionError, GitFICommit.parse_objname,
453
- "abcdefghij" * 4)
454
-
455
- def test_parse_path (self):
456
- """GitFICommit.parse_path() selftests."""
457
- self.assertEqual(GitFICommit.parse_path("foo/bar"), "foo/bar")
458
- self.assertEqual(GitFICommit.parse_path("path/with\n and \" in it"),
459
- '"path/with\\n and \\" in it"')
460
- self.assertRaises(AssertionError, GitFICommit.parse_path, 1)
461
- self.assertRaises(AssertionError, GitFICommit.parse_path, 0)
462
- self.assertRaises(AssertionError, GitFICommit.parse_path, -1)
463
- self.assertRaises(AssertionError, GitFICommit.parse_path, "foo//bar")
464
- self.assertRaises(AssertionError, GitFICommit.parse_path, "foo/bar/")
465
- self.assertRaises(AssertionError, GitFICommit.parse_path, "/foo/bar")
466
- self.assertRaises(AssertionError, GitFICommit.parse_path, "foo/./bar")
467
- self.assertRaises(AssertionError, GitFICommit.parse_path, "foo/../bar")
468
- self.assertRaises(AssertionError, GitFICommit.parse_path, "foo/bar/.")
469
- self.assertRaises(AssertionError, GitFICommit.parse_path, "foo/bar/..")
470
- self.assertRaises(AssertionError, GitFICommit.parse_path, "./foo/bar")
471
- self.assertRaises(AssertionError, GitFICommit.parse_path, "../foo/bar")
472
-
473
-
474
-class GitFastImport(object):
475
-
476
- """Encapsulate communication with git fast-import."""
477
-
478
- def __init__ (self, f, obj_fetcher, last_mark = 0):
479
- """Set up self to communicate with a fast-import process through f."""
480
- self.f = f # File object where fast-import stream is written
481
- self.obj_fetcher = obj_fetcher # GitObjectFetcher instance
482
- self.next_mark = last_mark + 1 # Next mark number
483
- self.refs = set() # Keep track of the refnames we've seen
484
-
485
- def comment (self, s):
486
- """Write the given comment in the fast-import stream."""
487
- assert "\n" not in s, "Malformed comment: '%s'" % (s)
488
- self.f.write("# %s\n" % (s))
489
-
490
- def commit (self, ref, commitdata):
491
- """Make a commit on the given ref, with the given GitFICommit.
492
-
493
- Return the mark number identifying this commit.
494
-
495
- """
496
- self.f.write("""\
497
-commit %(ref)s
498
-mark :%(mark)i
499
-committer %(name)s <%(email)s> %(timestamp)i %(timezone)s
500
-data %(msgLength)i
501
-%(msg)s
502
-""" % {
503
- 'ref': ref,
504
- 'mark': self.next_mark,
505
- 'name': commitdata.name,
506
- 'email': commitdata.email,
507
- 'timestamp': commitdata.timestamp,
508
- 'timezone': commitdata.timezone,
509
- 'msgLength': len(commitdata.message),
510
- 'msg': commitdata.message,
511
-})
512
-
513
- if ref not in self.refs:
514
- self.refs.add(ref)
515
- parent = ref + "^0"
516
- if self.obj_fetcher.get_sha1(parent):
517
- self.f.write("from %s\n" % (parent))
518
-
519
- for op in commitdata.pathops:
520
- self.f.write(" ".join(op))
521
- self.f.write("\n")
522
- self.f.write("\n")
523
- retval = self.next_mark
524
- self.next_mark += 1
525
- return retval
526
-
527
- def blob (self, data):
528
- """Import the given blob.
529
-
530
- Return the mark number identifying this blob.
531
-
532
- """
533
- self.f.write("blob\nmark :%i\ndata %i\n%s\n" %
534
- (self.next_mark, len(data), data))
535
- retval = self.next_mark
536
- self.next_mark += 1
537
- return retval
538
-
539
- def reset (self, ref, objname):
540
- """Reset the given ref to point at the given Git object."""
541
- self.f.write("reset %s\nfrom %s\n\n" %
542
- (ref, GitFICommit.parse_objname(objname)))
543
- if ref not in self.refs:
544
- self.refs.add(ref)
545
-
546
-
547
-class GitNotes(object):
548
-
549
- """Encapsulate access to Git notes.
550
-
551
- Simulates a dictionary of object name (SHA1) -> Git note mappings.
552
-
553
- """
554
-
555
- def __init__ (self, notes_ref, obj_fetcher):
556
- """Create a new Git notes interface, bound to the given notes ref."""
557
- self.notes_ref = notes_ref
558
- self.obj_fetcher = obj_fetcher # Used to get objects from repo
559
- self.imports = [] # list: (objname, note data blob name) tuples
560
-
561
- def __del__ (self):
562
- """Verify that self.commit_notes() was called before destruction."""
563
- if self.imports:
564
- error("Missing call to self.commit_notes().")
565
- error("%i notes are not committed!", len(self.imports))
566
-
567
- def _load (self, objname):
568
- """Return the note data associated with the given git object.
569
-
570
- The note data is returned in string form. If no note is found
571
- for the given object, None is returned.
572
-
573
- """
574
- try:
575
- f = self.obj_fetcher.open_obj("%s:%s" % (self.notes_ref, objname))
576
- ret = f.read()
577
- f.close()
578
- except KeyError:
579
- ret = None
580
- return ret
581
-
582
- def __getitem__ (self, objname):
583
- """Return the note contents associated with the given object.
584
-
585
- Raise KeyError if given object has no associated note.
586
-
587
- """
588
- blobdata = self._load(objname)
589
- if blobdata is None:
590
- raise KeyError("Object '%s' has no note" % (objname))
591
- return blobdata
592
-
593
- def get (self, objname, default = None):
594
- """Return the note contents associated with the given object.
595
-
596
- Return given default if given object has no associated note.
597
-
598
- """
599
- blobdata = self._load(objname)
600
- if blobdata is None:
601
- return default
602
- return blobdata
603
-
604
- def import_note (self, objname, data, gfi):
605
- """Tell git fast-import to store data as a note for objname.
606
-
607
- This method uses the given GitFastImport object to create a
608
- blob containing the given note data. Also an entry mapping the
609
- given object name to the created blob is stored until
610
- commit_notes() is called.
611
-
612
- Note that this method only works if it is later followed by a
613
- call to self.commit_notes() (which produces the note commit
614
- that refers to the blob produced here).
615
-
616
- """
617
- if not data.endswith("\n"):
618
- data += "\n"
619
- gfi.comment("Importing note for object %s" % (objname))
620
- mark = gfi.blob(data)
621
- self.imports.append((objname, mark))
622
-
623
- def commit_notes (self, gfi, author, message):
624
- """Produce a git fast-import note commit for the imported notes.
625
-
626
- This method uses the given GitFastImport object to create a
627
- commit on the notes ref, introducing the notes previously
628
- submitted to import_note().
629
-
630
- """
631
- if not self.imports:
632
- return
633
- commitdata = GitFICommit(author[0], author[1],
634
- time.time(), "0000", message)
635
- for objname, blobname in self.imports:
636
- assert isinstance(objname, int) and objname > 0
637
- assert isinstance(blobname, int) and blobname > 0
638
- commitdata.note(blobname, objname)
639
- gfi.commit(self.notes_ref, commitdata)
640
- self.imports = []
641
-
642
-
643
-class GitCachedNotes(GitNotes):
644
-
645
- """Encapsulate access to Git notes (cached version).
646
-
647
- Only use this class if no caching is done at a higher level.
648
-
649
- Simulates a dictionary of object name (SHA1) -> Git note mappings.
650
-
651
- """
652
-
653
- def __init__ (self, notes_ref, obj_fetcher):
654
- """Set up a caching wrapper around GitNotes."""
655
- GitNotes.__init__(self, notes_ref, obj_fetcher)
656
- self._cache = {} # Cache: object name -> note data
657
-
658
- def __del__ (self):
659
- """Verify that GitNotes' destructor is called."""
660
- GitNotes.__del__(self)
661
-
662
- def _load (self, objname):
663
- """Extend GitNotes._load() with a local objname -> note cache."""
664
- if objname not in self._cache:
665
- self._cache[objname] = GitNotes._load(self, objname)
666
- return self._cache[objname]
667
-
668
- def import_note (self, objname, data, gfi):
669
- """Extend GitNotes.import_note() with a local objname -> note cache."""
670
- if not data.endswith("\n"):
671
- data += "\n"
672
- assert objname not in self._cache
673
- self._cache[objname] = data
674
- GitNotes.import_note(self, objname, data, gfi)
675
-
676
-
677
-if __name__ == '__main__':
678
- unittest.main()