Mercurial and Git clients can push and pull from this alias URL to interact with this repository. You can change to which repository an alias points by going to the Aliases link on the project page.
# Copyright (C) 2011 Fog Creek Software. All rights reserved.## To enable the "kiln" extension put these lines in your ~/.hgrc:# [extensions]# kiln = /path/to/kiln.py## For help on the usage of "hg kiln" use:# hg help kiln# hg help -e kiln## This program is free software; you can redistribute it and/or modify# it under the terms of the GNU General Public License as published by# the Free Software Foundation; either version 2 of the License, or# (at your option) any later version.## This program is distributed in the hope that it will be useful,# but WITHOUT ANY WARRANTY; without even the implied warranty of# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the# GNU General Public License for more details.## You should have received a copy of the GNU General Public License# along with this program; if not, write to the Free Software# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.'''provides command-line support for working with KilnThis extension allows you to directly open up the Kiln page for yourrepository, including the annotation, file view, outgoing, and otherpages. Additionally, it will attempt to guess which remote Kilnrepository you wish push to and pull from based on its related repositories.This extension will also notify you when a Kiln server you access has anupdated version of the Kiln Client and Tools available.To disable the check for a version 'X.Y.Z' and all lower versions, add thefollowing line in the [kiln] section of your hgrc:: ignoreversion = X.Y.ZThis extension also lets you create or add changesets to a code review whenpushing to Kiln. See :hg:`help push` andhttp://kiln.stackexchange.com/questions/4679/ for more information.'''importitertoolsimportosimportreimportunicodedataimporturllibimporturllib2importsysimporttracebackfromcookielibimportMozillaCookieJarfromhashlibimportmd5frommercurialimportcommands,cmdutil,demandimport,extensions,hg, \
httprepo,localrepo,match,utilfrommercurialimportuiashguifrommercurialimporturlashgurlfrommercurial.errorimportRepoErrorfrommercurial.i18nimport_frommercurial.nodeimportnullrevtry:frommercurialimportscmutilexceptImportError:passdemandimport.disable()try:importjsonexceptImportError:sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)),'_custom'))importjsontry:importwebbrowserdefbrowse(url):webbrowser.open(escape_reserved(url))exceptImportError:ifos.name=='nt':importwin32apidefbrowse(url):win32api.ShellExecute(0,'open',escape_reserved(url),None,None,0)demandimport.enable()_did_version_check=FalseclassAPIError(Exception):def__init__(self,obj):'''takes a json object for debugging Inspect self.errors to see the API errors thrown. '''self.errors=dict((e['codeError'],e['sError'])foreinobj['errors']) def __str__(self):
return '\n'.join('%s: %s' % (k, v) for k, v in self.errors.items())
+class Review(dict):+ def __init__(self, json, ui=None, token=None, baseurl=None):+ self.ui = ui+ self.token = token+ self.baseurl = baseurl+ self.update(json)++ @property+ def version(self):+ return 2 if 'sReview' in self else 1++ @property+ def key(self):+ return str(self['sReview'] if self.version == 2 else self['ixReview'])++ def belongs_to(self, ixRepo):+ return ixRepo in self['ixRepos'] if self.version == 2 else self['ixRepo'] == ixRepo++ def associate(self, ixRepo, revs):+ if self.version == 1:+ params = {+ 'token': self.token,+ 'ixBug': self.key,+ 'revs': revs,+ }+ call_api(self.ui, self.baseurl, 'Api/1.0/Repo/%d/CaseAssociation/Create' % ixRepo, params, post=True)+ else:+ params = {+ 'token': self.token,+ 'revs': revs,+ 'ixRepo': ixRepo,+ }+ call_api(self.ui, self.baseurl, 'Api/2.0/Review/%s/Association/Create' % self.key, params, post=True)+ return urljoin(self.baseurl, 'Review', self.key)++ @classmethod+ def get_reviews(klass, ui, token, baseurl, ixRepo):+ review_lists = call_api(ui, baseurl, 'Api/2.0/Reviews', dict(token=token))+ reviews = {}+ for key, review_list in review_lists.iteritems():+ if not key.startswith('reviews'): continue+ for review in review_list:+ review = Review(review, ui, token, baseurl)+ if not review.belongs_to(ixRepo): continue+ reviews[review.key.lower()] = review+ return reviews+def urljoin(*components):
url = components[0]
for next in components[1:]:
ifnoturl.endswith('/'):url+='/'ifnext.startswith('/'):next=next[1:]url+=nextreturnurldef_baseurl(ui,path):remote=hg.repository(ui,path)try:# Mercurial >= 1.9url=util.removeauth(remote.url())exceptAttributeError:# Mercurial <= 1.8url=hgurl.removeauth(remote.url())ifurl.lower().find('/kiln/')>0orurl.lower().find('kilnhg.com/')>0:returnurlelse:returnNonedefescape_reserved(path):reserved=re.compile(r'^(((com[1-9]|lpt[1-9]|con|prn|aux)(\..*)?)|web\.config'+r'|clock\$|app_data|app_code|app_browsers'+r'|app_globalresources|app_localresources|app_themes'+r'|app_webreferences|bin|.*\.(cs|vb)html?)$',re.IGNORECASE)p=path.split('?')path=p[0]query='?'+p[1]iflen(p)>1else''return'/'.join('$'+part+'$'ifreserved.match(part)orpart.startswith('$')orpart.endswith('$')elsepartforpartinpath.split('/'))+querydefnormalize_name(s):returns.lower().replace(' ','-')defcall_api(ui,baseurl,urlsuffix,params,post=False):'''returns the json object for the url and the data dictionary Uses HTTP POST if the post parameter is True and HTTP GET otherwise. Raises APIError on API errors. '''url=baseurl+urlsuffixdata=urllib.urlencode(params,doseq=True)ui.debug(_('calling %s\n')%url,_(' with parameters %s\n')%params)try:ifpost:fd=urllib2.urlopen(url,data)else:fd=urllib2.urlopen(url+'?'+data) obj = json.load(fd)
except Exception:
ui.debug(_('kiln: traceback: %s\n') % traceback.format_exc())
- raise util.Abort(_('kiln: an error occurred while trying to reach %s\n') % url)
+ raise util.Abort(_('kiln: an error occurred while trying to reach %s') % url)
if isinstance(obj, dict) and 'errors' in obj:
if 'token' in params and obj['errors'][0]['codeError'] == 'InvalidToken':
token=login(ui,baseurl)add_kilnapi_token(ui,baseurl,token)params['token']=tokenreturncall_api(ui,baseurl,urlsuffix,params,post)raiseAPIError(obj)returnobjdeflogin(ui,url):ui.write(_('realm: %s\n')%url)user=ui.prompt('username:')pw=ui.getpass()token=call_api(ui,url,'Api/1.0/Auth/Login',dict(sUser=user,sPassword=pw))iftoken:returntokenraiseutil.Abort(_('authorization failed'))defget_domain(url):temp=url[url.find('://')+len('://'):]domain=temp[:temp.find('/')]port=Noneif':'indomain:domain,port=domain.split(':',1)if'.'notindomain:domain+='.local'returndomaindef_get_path(path):ifos.name=='nt':ret=os.path.expanduser('~\\_'+path)else:ret=os.path.expanduser('~/.'+path)# Cygwin's Python does not always expanduser() properly...ifre.match(r'^[A-Za-z]:',ret)isnotNoneandre.match(r'[A-Za-z]:\\',ret)isNone:ret=re.sub(r'([A-Za-z]):',r'\1:\\',ret)returnretdef_upgradecheck(ui,repo):global_did_version_checkif_did_version_checkornotui.configbool('kiln','autoupdate',True):return_did_version_check=True_upgrade(ui,repo)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)try:r=localrepo.localrepository(hgui.ui(),ext_dir)exceptRepoError:commands.init(hgui.ui(),dest=ext_dir)r=localrepo.localrepository(hgui.ui(),ext_dir)r.ui.setconfig('kiln','autoupdate',False)r.ui.pushbuffer()try:source='https://developers.kilnhg.com/Repo/Kiln/Group/Kiln-Extensions'ifcommands.incoming(r.ui,r,bundle=None,force=False,source=source)!=0:# no incoming changesets, or an error. Don't try to upgrade.ui.debug('kiln: no extensions upgrade available\n')returnui.write(_('updating Kiln Extensions at %s... ')%ext_dir)# pull and update return falsy values on successifcommands.pull(r.ui,r,source=source)orcommands.update(r.ui,r,clean=True):url=urljoin(repo.url()[:repo.url().lower().index('/repo')],'Tools')ui.write(_('unable to update\nvisit %s to download the newest extensions\n')%url)else:ui.write(_('complete\n'))exceptException,e:ui.debug(_('kiln: error updating extensions: %s\n')%e)ui.debug(_('kiln: traceback: %s\n')%traceback.format_exc())defis_dest_a_path(ui,dest):paths=ui.configitems('paths')forpathname,pathinpaths:ifpathname==dest:returnTruereturnFalsedefis_dest_a_scheme(ui,dest):destscheme=dest[:dest.find('://')]ifdestscheme:forschemeinhg.schemes:ifdestscheme==scheme:returnTruereturnFalsedefcreate_match_list(matchlist):ret=''forminmatchlist:ret+=' '+m+'\n'returnretdefget_username(url):url=re.sub(r'https?://','',url)url=re.sub(r'/.*','',url)if'@'inurl:# There should be some login info# rfind in case it's an email addressusername=url[:url.rfind('@')]if':'inusername:username=url[:url.find(':')]returnusername# Didn't find anything...return''defget_dest(ui):frommercurial.dispatchimport_parsetry:cmd_info=_parse(ui,sys.argv[1:])cmd=cmd_info[0]dest=cmd_info[2]ifdest:dest=dest[0]elifcmdin['outgoing','push']:dest='default-push'else:dest='default'except:dest='default'returnui.expandpath(dest)defcheck_kilnapi_token(ui,url):tokenpath=_get_path('hgkiln')if(notos.path.exists(tokenpath))oros.path.isdir(tokenpath):return''domain=get_domain(url)userhash=md5(get_username(get_dest(ui))).hexdigest()fp=open(tokenpath,'r')ret=""forlineinfp:try:d,u,t=line.split(' ')except:raiseutil.Abort(_('Authentication file %s is malformed.')%tokenpath)ifd==domainandu==userhash:# Get rid of that newline character...ret=t[:-1]fp.close()returnretdefadd_kilnapi_token(ui,url,fbToken):ifnotfbToken:returntokenpath=_get_path('hgkiln')ifos.path.isdir(tokenpath):raiseutil.Abort(_('Authentication file %s exists, but is a directory.')%tokenpath)domain=get_domain(url)userhash=md5(get_username(get_dest(ui))).hexdigest()fp=open(tokenpath,'a')fp.write(domain+' '+userhash+' '+fbToken+'\n')fp.close()defdelete_kilnapi_tokens():# deletes the hgkiln filetokenpath=_get_path('hgkiln')ifos.path.exists(tokenpath)andnotos.path.isdir(tokenpath):os.remove(tokenpath)defcheck_kilnauth_token(ui,url):cookiepath=_get_path('hgcookies')if(notos.path.exists(cookiepath))or(notos.path.isdir(cookiepath)):return''cookiepath=os.path.join(cookiepath,md5(get_username(get_dest(ui))).hexdigest())try:ifnotos.path.exists(cookiepath):return''cj=MozillaCookieJar(cookiepath)exceptIOError:return''domain=get_domain(url)cj.load(ignore_discard=True,ignore_expires=True)forcookieincj:ifdomain==cookie.domain:ifcookie.name=='fbToken':returncookie.valuedefremember_path(ui,repo,path,value):'''appends the path to the working copy's hgrc and backs up the original'''paths=dict(ui.configitems('paths'))# This should never happen.ifpathinpaths:return# ConfigParser only cares about these three characters.ifre.search(r'[:=\s]',path):returntry:audit_path=scmutil.pathauditor(repo.root)exceptImportError:audit_path=getattr(repo.opener,'audit_path',util.path_auditor(repo.root))audit_path('hgrc')audit_path('hgrc.backup')base=repo.opener.basehgrc,backup=[os.path.join(base,x)forxin'hgrc','hgrc.backup']ifos.path.exists(hgrc):util.copyfile(hgrc,backup)ui.setconfig('paths',path,value)try:fp=repo.opener('hgrc','a',text=True)# Mercurial assumes Unix newlines by default and so do we.fp.write('\n[paths]\n%s = %s\n'%(path,value))fp.close()exceptIOError:returndefunremember_path(ui,repo):'''restores the working copy's hgrc'''try:audit_path=scmutil.pathauditor(repo.root)exceptImportError:audit_path=getattr(repo.opener,'audit_path',util.path_auditor(repo.root))audit_path('hgrc')audit_path('hgrc.backup')base=repo.opener.basehgrc,backup=[os.path.join(base,x)forxin'hgrc','hgrc.backup']ifos.path.exists(backup):util.copyfile(backup,hgrc)else:os.remove(hgrc)defguess_kilnpath(orig,ui,repo,dest=None,**opts):ifnotdest:returnorig(ui,repo,**opts)ifos.path.exists(dest)oris_dest_a_path(ui,dest)oris_dest_a_scheme(ui,dest):returnorig(ui,repo,dest,**opts)else:targets=get_targets(repo)matches=[]prefixmatches=[]fortargetintargets:url='%s/%s/%s/%s'%(target[0],target[1],target[2],target[3])ndest=normalize_name(dest)ntarget=[normalize_name(t)fortintarget[1:4]]aliases=[normalize_name(s)forsintarget[4]]ifndest.count('/')==0and \
(ntarget[0]==ndestor \
ntarget[1]==ndestor \
ntarget[2]==ndestor \
ndestinaliases):matches.append(url)elifndest.count('/')==1and \
'/'.join(ntarget[0:2])==ndestor \
'/'.join(ntarget[1:3])==ndest:matches.append(url)elifndest.count('/')==2and \
'/'.join(ntarget[0:3])==ndest:matches.append(url)if(ntarget[0].startswith(ndest)or \
ntarget[1].startswith(ndest)or \
ntarget[2].startswith(ndest)or \
'/'.join(ntarget[0:2]).startswith(ndest)or \
'/'.join(ntarget[1:3]).startswith(ndest)or \
'/'.join(ntarget[0:3]).startswith(ndest)):prefixmatches.append(url)iflen(matches)==0:iflen(prefixmatches)==0:# if there are no matches at all, let's just let mercurial handle it.returnorig(ui,repo,dest,**opts)else:urllist=create_match_list(prefixmatches)raiseutil.Abort(_('%s did not exactly match any part of the repository slug:\n\n%s')%(dest,urllist))eliflen(matches)>1:urllist=create_match_list(matches)raiseutil.Abort(_('%s matches more than one Kiln repository:\n\n%s')%(dest,urllist))# Unique match -- perform the operationtry:remember_path(ui,repo,dest,matches[0])returnorig(ui,repo,matches[0],**opts)finally:unremember_path(ui,repo)defget_tails(repo):tails=[]forrevinxrange(repo['tip'].rev()+1):ctx=repo[rev]ifctx.p1().rev()==nullrevandctx.p2().rev()==nullrev:tails.append(ctx.hex())ifnotlen(tails):raiseutil.Abort(_('Path guessing is only enabled for non-empty repositories.'))returntailsdefget_targets(repo):targets=[]kilnschemes=repo.ui.configitems('kiln_scheme')forschemeinkilnschemes:url=scheme[1]baseurl=get_api_url(url)tails=get_tails(repo)token=get_token(repo.ui,baseurl)# We have an token at this pointparams=dict(revTails=tails,token=token)related_repos=call_api(repo.ui,baseurl,'Api/1.0/Repo/Related',params)targets.extend([[url,related_repo['sProjectSlug'],related_repo['sGroupSlug'],related_repo['sSlug'],related_repo.get('rgAliases',[])]forrelated_repoinrelated_repos])returntargetsdefdisplay_targets(repo):targets=get_targets(repo)repo.ui.write(_('The following Kiln targets are available for this repository:\n\n'))fortargetintargets:iftarget[4]:alias_text=_(' (alias%s: %s)')%('es'iflen(target[4])>1else'',', '.join(target[4]))else:alias_text=''repo.ui.write(' %s/%s/%s/%s%s\n'%(target[0],target[1],target[2],target[3],alias_text))defget_token(ui,url):'''Checks for an existing API token. If none, returns a new valid token.'''token=check_kilnapi_token(ui,url)ifnottoken:token=check_kilnauth_token(ui,url)add_kilnapi_token(ui,url,token)ifnottoken:token=login(ui,url)add_kilnapi_token(ui,url,token)returntokendefget_api_url(url):'''Given a URL, returns the URL of the Kiln installation.'''if'/kiln/'inurl.lower():baseurl=url[:url.lower().find('/kiln/')+6]elif'kilnhg.com/'inurl.lower():baseurl=url[:url.lower().find('kilnhg.com/')+11]else:baseurl=urlreturnbaseurlclassHTTPNoRedirectHandler(urllib2.HTTPRedirectHandler):defhttp_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 pagereturnfphttp_error_301=http_error_303=http_error_307=http_error_302defget_repo_record(repo,url,token=None):'''Returns a Kiln repository record that corresponds to the given repo.'''baseurl=get_api_url(url)ifnottoken: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 URLif'location'infd.headers:url=fd.headers.getheaders('location')[0]elif'uri'infd.headers:url=fd.headers.getheaders('uri')[0]excepturllib2.HTTPError:raiseutil.Abort(_('Invalid URL: %s')%url)deffind_slug(slug,l,attr=None):ifnotl:returnNoneforcandidateinl.get(attr)ifattrelsel:ifcandidate['sSlug']==slugor(slug=='Group'andcandidate['sSlug']==''):returncandidatereturnNonepaths=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')returnrepodefnew_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\n')%url)try:returncall_api(repo.ui,baseurl,'Api/1.0/Repo/Create',params,post=True)exceptAPIError,e:if'RepoNameAlreadyUsed'ine.errors:repo.ui.warn(_('error: kiln: a repo with this name already exists: %s\n')%name)returnraisedefnormalize_user(s):'''Takes a Unicode string and returns an ASCII string.'''returnunicodedata.normalize('NFKD',s).encode('ASCII','ignore')defencode_out(s):'''Takes a Unicode string and returns a string encoded for output.'''returns.encode(sys.stdout.encoding,'ignore')defrecord_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)defwalk(repo,revs):'''Returns revisions in repo specified by the string revs'''returncmdutil.walkchangerevs(repo,match.always(repo.root,None),{'rev':[revs.encode('ascii','ignore')]},lambda*args:None)defprint_list(ui,l,header):'''Prints a list l to ui using list notation, with header being the first line'''ui.write(_('%s\n'%header))foriteminl:ui.write(_('- %s\n')%item)defwrap_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)defadd_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.'''ifname_to_ix[reviewer]inreviewers: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]forrinreviewers],'reviewers:')defreview(ui,repo,dest,opts):'''Associates the pushed changesets with a new or existing Kiln review.'''ifnotopts['review']ornotrepo.ui.config('kiln','node'):returnurl=repo.ui.expandpath(destifdestelse'default-push')ifurl=='default-push':raiseutil.Abort(_('kiln: please specify a default-push path before using --review'))baseurl=get_api_url(url)ifbaseurl==url:ui.write_err(_('kiln: warning: this does not appear to be a Kiln URL: %s\n')%baseurl) token = get_token(ui, baseurl)
kiln_repo = get_repo_record(repo, url, token)
+ reviews = Review.get_reviews(repo.ui, token, baseurl, kiln_repo['ixRepo'])- 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']))
+ for review_key in sorted(reviews.iterkeys()):
+ review = reviews[review_key]+ title = re.sub('\s+', ' ', review['sTitle'])+ ui.write(encode_out(_('%s - %s\n') % (review_key, title)))
+ choices.append(review_key)
choices.extend(['n', 'q', '?'])
while True:
choice=ui.prompt(_('add to review? [nq?]')).lower()ifchoicenotinchoices:ui.write(_('unrecognized response\n\n'))elifchoice=='q':returnelifchoice=='?':exist_review=_(' - enter an existing review number to add changeset(s).\n')ifreviewselse''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 existingbreaknode=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)forrinheads]ifheadselse['%s:tip'%node]revset=' or '.join(sets)revs=[r.hex()forrinwalk(repo,revset)]ifchoice=='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'])forpinpeople_records])ix_to_name=dict([(p['ixPerson'],encode_out(p['sName']))forpinpeople_records])reviewers=[]whileTrue:reviewer=ui.prompt(_('\nchoose reviewer(s) [dlq?]'),default='')reviewer=normalize_user(unicode(reviewer,sys.stdin.encoding)).lower()ifreviewer=='q':returnelifreviewer=='?':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'))elifreviewer=='l':ifreviewers:print_list(ui,[ix_to_name[r]forrinreviewers],'reviewers:')else:ui.write(_('no users selected.\n'))elifreviewer=='d':ifreviewers:breakelse:ui.write(_('no users selected.\n'))elifreviewerinname_to_ix.keys():add_unique_reviewer(ui,reviewer,reviewers,name_to_ix,ix_to_name)else:options=filter(lambdaname:reviewerinname,name_to_ix.keys())options=[ix_to_name[name_to_ix[name]]fornameinoptions]options=sorted(options,key=lambdan:n.lower())ifoptions:iflen(options)==1:# If one user matches search, just add him/heroption=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']))))
+ r = Review(call_api(repo.ui, baseurl, 'Api/2.0/Review/Create', params, post=True))
+ ui.write(_('new review created: %s\n' % urljoin(baseurl, 'Review', r.key)))
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)))
+url = reviews[choice].associate(kiln_repo['ixRepo'], revs)
+ ui.write(_('updated review: %s\n' % url))
def dummy_command(ui, repo, dest=None, **opts):
'''dummy command to pass to guess_path() for hg kiln
Returns the repository URL if dest has been successfully path guessed, None otherwise. '''returnopts['path']!=destanddestorNonedef_standin_expand(paths):'''given a sequence of filenames, returns a set of filenames -- relative to the current working directory! -- prefixed with all possible standin prefixes e.g. .hglf or .kbf in addition to the originals'''paths=[os.path.relpath(os.path.abspath(p),os.getcwd())forpinpaths]choices=[[p,os.path.join('.kbf',p),os.path.join('.hglf',p)]forpinpaths]returnset(itertools.chain(*choices))def_filename_match(repo,ctx,paths):'''returns a set of filenames contained in both paths and the ctx's manifest, accounting for standins'''try:match=scmutil.match(ctx,paths)match.bad=lambda*a:Nonepaths=set(ctx.walk(match))returnpathsexceptImportError:# Make every path normalized and relative to the current# working directory, similar to scmutil.needles=set(map(os.path.normpath,paths))haystacks=[os.path.relpath(os.path.join(repo.root,p),os.getcwd())forpinctx.manifest().iterkeys()]haystacks=set(map(os.path.normpath,haystacks))returnneedles.intersection(haystacks)defkiln(ui,repo,**opts):'''show the relevant page of the repository in Kiln This command allows you to navigate straight to the Kiln page for a repository, including directly to settings, file annotation, and file & changeset viewing. Typing "hg kiln" by itself will take you directly to the repository history in kiln. Specify any other options to override this default. The --rev, --annotate, --file, and --filehistory options can be used together. To display a list of valid targets, type hg kiln --targets. To push or pull from one of these targets, use any unique identifier from this list as the parameter to the push/pull command. '''try:url=_baseurl(ui,ui.expandpath(opts['path']or'default',opts['path']or'default-push'))exceptRepoError:url=guess_kilnpath(dummy_command,ui,repo,dest=opts['path'],**opts)ifnoturl:raiseifnoturl:raiseutil.Abort(_('this does not appear to be a Kiln-hosted repository\n'))default=Truedeffiles(key):paths=_filename_match(repo,repo['.'],_standin_expand(opts[key]))ifnotpaths:ui.warn(_('error: kiln: cannot find any paths matching %s\n')%', '.join(opts[key]))iflen(paths)>5:# If we're passed a directory, we should technically open# a tab for each file in that directory because that's how# other hg commands e.g. cat work. However, since that's# quite annoying to do by accident when opening browsers,# let's prompt. (This is only relevant when scmutil# exists.)char=ui.prompt(_('about to open %d browser tabs or windows, abort? [Yn]')%len(paths)).lower()ifchar!='n':raiseSystemExit(0)returnpathsifopts['rev']:default=Falseforctxin(repo[rev]forrevinopts['rev']):browse(urljoin(url,'History',ctx.hex()))ifopts['annotate']:default=Falseforfinfiles('annotate'):browse(urljoin(url,'File',f)+'?view=annotate')ifopts['file']:default=Falseforfinfiles('file'):browse(urljoin(url,'File',f))ifopts['filehistory']:default=Falseforfinfiles('filehistory'):browse(urljoin(url,'FileHistory',f)+'?rev=tip')ifopts['outgoing']:default=Falsebrowse(urljoin(url,'Outgoing'))ifopts['settings']:default=Falsebrowse(urljoin(url,'Settings'))ifopts['targets']:default=Falsedisplay_targets(repo)ifopts['new_branch']:default=Falsenew_branch(repo,url,opts['new_branch'])ifopts['logout']:default=Falsedelete_kilnapi_tokens()ifdefaultoropts['changes']:browse(url)defuisetup(ui):extensions.wrapcommand(commands.table,'outgoing',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 tablepush_cmd[1].extend([('','review',None,'associate changesets with Kiln review')])defreposetup(ui,repo):ifissubclass(repo.__class__,httprepo.httprepository):_upgradecheck(ui,repo)repo.ui.setconfig('hooks','outgoing.kilnreview','python:kiln.record_base')defextsetup(ui):try:g=extensions.find('gestalt')extensions.wrapcommand(g.cmdtable,'overview',guess_kilnpath)extensions.wrapcommand(g.cmdtable,'advice',guess_kilnpath)extensions.wrapcommand(g.cmdtable,'next',guess_kilnpath)exceptKeyError:passtry:f=extensions.find('fetch')extensions.wrapcommand(f.cmdtable,'fetch',guess_kilnpath)exceptKeyError:passcmdtable={'kiln':(kiln,[('a','annotate',[],_('annotate the file provided')),('c','changes',None,_('view the history of this repository; this is the default')),('f','file',[],_('view the file contents')),('l','filehistory',[],_('view the history of the file')),('o','outgoing',None,_('view the repository\'s outgoing tab')),('s','settings',None,_('view the repository\'s settings tab')),('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|-n branchName|--logout]'))}
Attach a Trello Card
Add a tag
Your session has expired
You are no longer logged in. Please log in and try your request again.
Filter RSS Feed
This RSS feed URL allows you to see the contents of your current filter using any feed reader.
This link includes a special authentication token. If you share the URL with anyone else, they can see this RSS feed's activity. You can disable these tokens when needed.
Your current filter is unsaved; changing it won't affect this RSS feed.