Changeset 67d19149537f…
Parent f0fe708a7d75…
by Benjamin Pollack <benjamin@fogcreek.com>
Changes to 5 files · Browse files at 67d19149537f Showing diff from parent f0fe708a7d75 Diff from another changeset...
@@ -472,7 +472,11 @@ def httpsendfile(ui, filename):
try:
# Mercurial >= 1.9
- return httpconnection.httpsendfile(ui, filename, 'rb')
+ sendfile = httpconnection.httpsendfile(ui, filename, 'rb')
+ if getattr(sendfile, '__len__', None) is None:
+ # Mercurial 1.9.3 removes httpsendfile's __len__. Hack it back in.
+ setattr(sendfile.__class__, '__len__', lambda self: self.length)
+ return sendfile
except ImportError:
if 'ui' in inspect.getargspec(url_.httpsendfile.__init__)[0]:
# Mercurial == 1.8
|
@@ -19,16 +19,12 @@ from mercurial import cmdutil, commands, hg, extensions
from mercurial.i18n import _
-try:
- from mercurial import discovery
-except ImportError:
- pass
-
max_push_size = 1000
def findoutgoing(repo, other):
try:
# Mercurial 1.6 through 1.8
+ from mercurial import discovery
return discovery.findoutgoing(repo, other, force=False)
except AttributeError:
# Mercurial 1.9 and higher
|
|
|
@@ -34,16 +34,19 @@ following line in the [kiln] section of your hgrc:
ignoreversion = X.Y.Z
'''
+import httplib
import os
import re
+import unicodedata
import urllib
import urllib2
import sys
+import traceback
from cookielib import MozillaCookieJar
from hashlib import md5
-from mercurial import commands, demandimport, extensions, hg, httprepo, \
- localrepo, match, util
+from mercurial import commands, cmdutil, demandimport, error, extensions, hg, \
+ httprepo, localrepo, match, util
from mercurial import ui as hgui
from mercurial import url as hgurl
from mercurial.error import RepoError
@@ -137,6 +140,8 @@ '''
url = baseurl + urlsuffix
data = urllib.urlencode(params, doseq=True)
+ ui.debug(_('calling %s\n') % url,
+ _(' with parameters %s\n') % params)
try:
if post:
fd = urllib2.urlopen(url, data)
@@ -144,9 +149,8 @@ fd = urllib2.urlopen(url + '?' + data)
obj = json.load(fd)
except Exception:
- raise util.Abort(_('Path guessing requires Fog Creek Kiln 2.0. If you'
- ' are running Kiln 2.0 and continue to experience'
- ' problems, please contact Fog Creek Software.'))
+ ui.debug(_('kiln: traceback: %s\n') % traceback.format_exc())
+ raise util.Abort(_('kiln: an error occurred while trying to reach %s\n') % url)
if isinstance(obj, dict) and 'errors' in obj:
if 'token' in params and obj['errors'][0]['codeError'] == 'InvalidToken':
@@ -198,7 +202,7 @@
def _upgrade(ui, repo):
ext_dir = os.path.dirname(os.path.abspath(__file__))
- ui.debug('kiln: checking for extensions upgrade for %s\n' % ext_dir)
+ ui.debug(_('kiln: checking for extensions upgrade for %s\n') % ext_dir)
try:
r = localrepo.localrepository(hgui.ui(), ext_dir)
@@ -222,7 +226,8 @@ else:
ui.write(_('complete\n'))
except Exception, e:
- ui.debug(_('kiln: error updating Kiln Extensions: %s\n') % e)
+ ui.debug(_('kiln: error updating extensions: %s\n') % e)
+ ui.debug(_('kiln: traceback: %s\n') % traceback.format_exc())
def is_dest_a_path(ui, dest):
paths = ui.configitems('paths')
@@ -355,8 +360,11 @@ audit_path('hgrc')
audit_path('hgrc.backup')
base = repo.opener.base
- util.copyfile(os.path.join(base, 'hgrc'),
- os.path.join(base, 'hgrc.backup'))
+
+ hgrc, backup = [os.path.join(base, x) for x in 'hgrc', 'hgrc.backup']
+ if os.path.exists(hgrc):
+ util.copyfile(hgrc, backup)
+
ui.setconfig('paths', path, value)
try:
@@ -378,9 +386,12 @@ audit_path('hgrc')
audit_path('hgrc.backup')
base = repo.opener.base
- if os.path.exists(os.path.join(base, 'hgrc')):
- util.copyfile(os.path.join(base, 'hgrc.backup'),
- os.path.join(base, 'hgrc'))
+
+ hgrc, backup = [os.path.join(base, x) for x in 'hgrc', 'hgrc.backup']
+ if os.path.exists(backup):
+ util.copyfile(backup, hgrc)
+ else:
+ os.remove(hgrc)
def guess_kilnpath(orig, ui, repo, dest=None, **opts):
if not dest:
@@ -389,7 +400,7 @@ if os.path.exists(dest) or is_dest_a_path(ui, dest) or is_dest_a_scheme(ui, dest):
return orig(ui, repo, dest, **opts)
else:
- targets = get_targets(repo);
+ targets = get_targets(repo)
matches = []
prefixmatches = []
@@ -454,22 +465,10 @@ kilnschemes = repo.ui.configitems('kiln_scheme')
for scheme in kilnschemes:
url = scheme[1]
- if url.lower().find('/kiln/') != -1:
- baseurl = url[:url.lower().find('/kiln/') + len("/kiln/")]
- elif url.lower().find('kilnhg.com/') != -1:
- baseurl = url[:url.lower().find('kilnhg.com/') + len("kilnhg.com/")]
- else:
- continue
+ baseurl = get_api_url(url)
tails = get_tails(repo)
-
- token = check_kilnapi_token(repo.ui, baseurl)
- if not token:
- token = check_kilnauth_token(repo.ui, baseurl)
- add_kilnapi_token(repo.ui, baseurl, token)
- if not token:
- token = login(repo.ui, baseurl)
- add_kilnapi_token(repo.ui, baseurl, token)
+ token = get_token(repo.ui, baseurl)
# We have an token at this point
params = dict(revTails=tails, token=token)
@@ -491,6 +490,231 @@ alias_text = ''
repo.ui.write(' %s/%s/%s/%s%s\n' % (target[0], target[1], target[2], target[3], alias_text))
+def get_token(ui, url):
+ '''Checks for an existing API token. If none, returns a new valid token.'''
+ token = check_kilnapi_token(ui, url)
+ if not token:
+ token = check_kilnauth_token(ui, url)
+ add_kilnapi_token(ui, url, token)
+ if not token:
+ token = login(ui, url)
+ add_kilnapi_token(ui, url, token)
+ return token
+
+def get_api_url(url):
+ '''Given a URL, returns the URL of the Kiln installation.'''
+ if '/kiln/' in url.lower():
+ baseurl = url[:url.lower().find('/kiln/') + 6]
+ elif 'kilnhg.com/' in url.lower():
+ baseurl = url[:url.lower().find('kilnhg.com/') + 11]
+ else:
+ baseurl = url
+ return baseurl
+
+class HTTPNoRedirectHandler(urllib2.HTTPRedirectHandler):
+ def http_error_302(self, req, fp, code, msg, headers):
+ # Doesn't allow multiple redirects so repo alias URLs will not
+ # eventually get redirected to the unhelpful login page
+ return fp
+
+ http_error_301 = http_error_303 = http_error_307 = http_error_302
+
+def get_repo_record(repo, url, token=None):
+ '''Returns a Kiln repository record that corresponds to the given repo.'''
+ baseurl = get_api_url(url)
+ if not token:
+ token = get_token(repo.ui, baseurl)
+
+ try:
+ data = urllib.urlencode({ 'token': token }, doseq=True)
+ opener = urllib2.build_opener(HTTPNoRedirectHandler)
+ urllib2.install_opener(opener)
+ fd = urllib2.urlopen(url + '?' + data)
+
+ # Get redirected URL
+ if 'location' in fd.headers:
+ url = fd.headers.getheaders('location')[0]
+ elif 'uri' in fd.headers:
+ url = fd.headers.getheaders('uri')[0]
+ except HTTPError as e:
+ raise util.Abort(_('Invalid URL: %s' % url))
+
+ def find_slug(slug, l, attr=None):
+ if not l:
+ return None
+ for candidate in l.get(attr) if attr else l:
+ if candidate['sSlug'] == slug or (slug == 'Group' and candidate['sSlug'] == ''):
+ return candidate
+ return None
+
+ paths = url.split('/')
+ kiln_projects = call_api(repo.ui, baseurl, 'Api/1.0/Project/', dict(token=token))
+ project, group, repo = paths[-3:]
+ project = find_slug(project, kiln_projects)
+ group = find_slug(group, project, 'repoGroups')
+ repo = find_slug(repo, group, 'repos')
+ return repo
+
+def new_branch(repo, url, name):
+ '''Creates a new, decentralized branch off of the specified repo.'''
+ baseurl = get_api_url(url)
+ token = get_token(repo.ui, baseurl)
+ kiln_repo = get_repo_record(repo, url, token)
+ params = {'sName': name,
+ 'ixRepoGroup': kiln_repo['ixRepoGroup'],
+ 'ixParent': kiln_repo['ixRepo'],
+ 'fCentral': False,
+ 'sDefaultPermission': 'inherit',
+ 'token': token}
+ repo.ui.write('branching from %s' % url)
+ return call_api(repo.ui, baseurl, 'Api/1.0/Repo/Create', params, post=True)
+
+def normalize_user(s):
+ '''Takes a Unicode string and returns an ASCII string.'''
+ return unicodedata.normalize('NFKD', s).encode('ASCII', 'ignore')
+
+def encode_out(s):
+ '''Takes a Unicode string and returns a string encoded for output.'''
+ return s.encode(sys.stdout.encoding, 'ignore')
+
+def record_base(ui, repo, node, **kwargs):
+ '''Stores the first changeset committed in the repo UI so we do not need to expensively recalculate.'''
+ repo.ui.setconfig('kiln', 'node', node)
+
+def walk(repo, revs):
+ '''Returns revisions in repo specified by the string revs'''
+ return cmdutil.walkchangerevs(repo, match.always(repo.root, None), {'rev': [revs.encode('ascii', 'ignore')]}, lambda *args: None)
+
+def print_list(ui, l, header):
+ '''Prints a list l to ui using list notation, with header being the first line'''
+ ui.write(_('%s\n' % header))
+ for item in l:
+ ui.write(_('- %s\n') % item)
+
+def wrap_push(orig, ui, repo, dest=None, **opts):
+ '''Wraps `hg push' so a review will be created after path guessing and a successful push.'''
+ guess_kilnpath(orig, ui, repo, dest, **opts)
+ review(ui, repo, dest, opts)
+
+def add_unique_reviewer(ui, reviewer, reviewers, name_to_ix, ix_to_name):
+ '''Adds a reviewer to reviewers if it is not already added. Otherwise, print an error.'''
+ if name_to_ix[reviewer] in reviewers:
+ ui.write(_('user already added: %s\n') % ix_to_name[name_to_ix[reviewer]])
+ else:
+ reviewers.append(name_to_ix[reviewer])
+ print_list(ui, [ix_to_name[r] for r in reviewers], 'reviewers:')
+
+def review(ui, repo, pats, opts):
+ '''Associates the pushed changesets with a new or existing Kiln review.'''
+ if not opts['review'] or not repo.ui.config('kiln', 'node'):
+ return
+
+ url = repo.ui.expandpath(pats[0] if pats else 'default-push', default='default')
+ baseurl = get_api_url(url)
+ token = get_token(ui, baseurl)
+ kiln_repo = get_repo_record(repo, url, token)
+
+ review_lists = call_api(repo.ui, baseurl, 'Api/1.0/Reviews', dict(token=token))
+ reviews = filter(lambda r: r['ixRepo'] == kiln_repo['ixRepo'], review_lists['reviewsOpenedByMe'] + review_lists['reviewsReviewedByMe'])
+ reviews = sorted(reviews, key=lambda r: r['ixReview'])
+ choices = []
+ ui.write(_('\n'))
+ for r in reviews:
+ ui.write(encode_out(_('%d - %s\n') % (r['ixReview'], r['sTitle'])))
+ choices.append(str(r['ixReview']))
+
+ choices.extend(['n', 'q', '?'])
+ while True:
+ choice = ui.prompt(_('add to review? [nq?]')).lower()
+ if choice not in choices:
+ ui.write(_('unrecognized response\n\n'))
+ elif choice == 'q':
+ return
+ elif choice == '?':
+ exist_review = _(' - enter an existing review number to add changeset(s).\n') if reviews else ''
+ ui.write(exist_review)
+ ui.write(_('n - new, create a new review.\n'),
+ _('q - quit, do not associate changeset(s).\n'),
+ _('? - display help.\n\n'))
+ else:
+ # Create new review or associate changeset(s) to existing
+ break
+
+ node = repo.ui.config('kiln', 'node')
+ heads = opts['rev']
+ # If no specified revisions to push, default to getting revision numbers (not ancestors/descendants) between node and tip.
+ sets = ['%s::%s' % (node, r) for r in heads] if heads else ['%s:tip' % node]
+ revset = ' or '.join(sets)
+ revs = [r.hex() for r in walk(repo, revset)]
+
+ if choice == 'n':
+ # Associate changeset(s) with a new review.
+ people_records = call_api(repo.ui, baseurl, 'Api/1.0/Person', dict(token=token))
+ # If two user names normalize to the same string, then name_to_ix will only store the second person. This will
+ # also affect user input if the user enters the first user's unstored name, then the user will add the wrong
+ # reviewer. If this edge case becomes an issue, I wish thee happy pondering.
+ name_to_ix = dict([(normalize_user(p['sName']).lower(), p['ixPerson']) for p in people_records])
+ ix_to_name = dict([(p['ixPerson'], encode_out(p['sName'])) for p in people_records])
+
+ reviewers = []
+ while True:
+ reviewer = ui.prompt(_('\nchoose reviewer(s) [dlq?]'), default='')
+ reviewer = normalize_user(unicode(reviewer, sys.stdin.encoding)).lower()
+ if reviewer == 'q':
+ return
+ elif reviewer == '?':
+ ui.write(_(' - type a user\'s full name to add that user. a partial name displays\n'),
+ _(' a list of matching users.\n'),
+ _('d - done, create a new review.\n'),
+ _('l - list, list current reviewers.\n'),
+ _('q - quit, do not create a new review.\n'),
+ _('? - display help.\n'))
+ elif reviewer == 'l':
+ if reviewers:
+ print_list(ui, [ix_to_name[r] for r in reviewers], 'reviewers:')
+ else:
+ ui.write(_('no users selected.\n'))
+ elif reviewer == 'd':
+ if reviewers:
+ break
+ else:
+ ui.write(_('no users selected.\n'))
+ elif reviewer in name_to_ix.keys():
+ add_unique_reviewer(ui, reviewer, reviewers, name_to_ix, ix_to_name)
+ else:
+ options = filter(lambda name: reviewer in name, name_to_ix.keys())
+ options = [ix_to_name[name_to_ix[name]] for name in options]
+ options = sorted(options, key=lambda n: n.lower())
+ if options:
+ if len(options) == 1:
+ # If one user matches search, just add him/her
+ option = normalize_user(unicode(options[0], sys.stdout.encoding)).lower()
+ add_unique_reviewer(ui, option, reviewers, name_to_ix, ix_to_name)
+ else:
+ print_list(ui, options, 'user names (%d) that match:' % len(options))
+ else:
+ ui.write(_('no matching users.\n'))
+
+ params = {
+ 'token': token,
+ 'ixRepo': kiln_repo['ixRepo'],
+ 'revs': revs,
+ 'ixReviewers': reviewers,
+ 'sTitle': '(Multiple changesets)' if len(revs) > 1 else repo[revs[0]].description(),
+ 'sDescription': 'Review created from push.'
+ }
+ r = call_api(repo.ui, baseurl, 'Api/1.0/Review/Create', params, post=True)
+ ui.write(_('new review created: %s\n' % urljoin(baseurl, 'Review', str(r['ixReview']))))
+ else:
+ # Associate changeset(s) with an existing review.
+ params = {
+ 'token': token,
+ 'ixBug': int(choice),
+ 'revs': revs
+ }
+ call_api(repo.ui, baseurl, 'Api/1.0/Repo/%d/CaseAssociation/Create' % kiln_repo['ixRepo'], params, post=True)
+ ui.write(_('updated review: %s\n' % urljoin(baseurl, 'Review', choice)))
+
def dummy_command(ui, repo, dest=None, **opts):
'''dummy command to pass to guess_path() for hg kiln
@@ -499,10 +723,10 @@ '''
return opts['path'] != dest and dest or None
-def kiln(ui, repo, *pats, **opts):
+def kiln(ui, repo, **opts):
'''show the relevant page of the repository in Kiln
- This command allows you to navigate straight the Kiln page for a
+ This command allows you to navigate straight to the Kiln page for a
repository, including directly to settings, file annotation, and
file & changeset viewing.
@@ -565,6 +789,9 @@ if opts['targets']:
default = False
display_targets(repo)
+ if opts['new_branch']:
+ default = False
+ new_branch(repo, url, opts['new_branch'])
if opts['logout']:
default = False
delete_kilnapi_tokens()
@@ -574,13 +801,16 @@
def uisetup(ui):
extensions.wrapcommand(commands.table, 'outgoing', guess_kilnpath)
- extensions.wrapcommand(commands.table, 'push', guess_kilnpath)
extensions.wrapcommand(commands.table, 'pull', guess_kilnpath)
extensions.wrapcommand(commands.table, 'incoming', guess_kilnpath)
+ push_cmd = extensions.wrapcommand(commands.table, 'push', wrap_push)
+ # Add --review as a valid flag to push's command table
+ push_cmd[1].extend([('', 'review', None, 'associate changesets with Kiln review')])
def reposetup(ui, repo):
if issubclass(repo.__class__, httprepo.httprepository):
_upgradecheck(ui, repo)
+ repo.ui.setconfig('hooks', 'outgoing.kilnreview', 'python:kiln.record_base')
def extsetup(ui):
try:
@@ -609,6 +839,7 @@ ('p', 'path', '', _('select which Kiln branch of the repository to use')),
('r', 'rev', [], _('view the specified changeset in Kiln')),
('t', 'targets', None, _('view the repository\'s targets')),
+ ('n', 'new-branch', '', _('asynchronously create a new branch from the current repository')),
('', 'logout', None, _('log out of Kiln sessions'))],
- _('hg kiln [-p url] [-r rev|-a file|-f file|-c|-o|-s|-t|--logout]'))
+ _('hg kiln [-p url] [-r rev|-a file|-f file|-c|-o|-s|-t|-n branchName|--logout]'))
}
|
@@ -53,6 +53,7 @@
from mercurial.i18n import _
import mercurial.url
+from mercurial import commands
current_user = None
@@ -205,7 +206,7 @@ return urlopener
mercurial.url.opener = opener
-def logout(ui, repo, domain=None):
+def logout(ui, domain=None):
"""log out of http repositories
Clears the cookies stored for HTTP repositories. If [domain] is
@@ -220,6 +221,8 @@ except KeyError:
ui.write("Not logged in to '%s'\n" % (domain,))
+commands.norepo += ' logout'
+
cmdtable = {
'logout': (logout, [], '[domain]')
}
|
@@ -1,11 +1,10 @@ import compileall
import os
-import win32api
import zipfile
folders = ['bfiles', '_custom']
extensions = ['.py']
-excludes = ['\\setup.py']
+excludes = ['setup.py']
def compile_extensions():
compileall.compile_dir(os.path.dirname(__file__), force=1)
@@ -42,7 +41,7 @@ files = list_files(absdir, '.')
print 'Creating ZIP archive...'
- zip = zipfile.ZipFile(absdir + '\kiln_extensions.zip', 'w')
+ zip = zipfile.ZipFile(os.path.join(absdir, 'kiln_extensions.zip'), 'w')
for file in files:
zip.write(file[0], file[1])
zip.close()
|
Loading...