Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. #!/usr/bin/env python3
  2. #
  3. # Barnum, a Patchset Tool (pt)
  4. #
  5. # This Git wrapper script is designed to reduce the ceremony of working with Gitblit patchsets.
  6. #
  7. # Copyright 2014 gitblit.com.
  8. #
  9. # Licensed under the Apache License, Version 2.0 (the "License");
  10. # you may not use this file except in compliance with the License.
  11. # You may obtain a copy of the License at
  12. #
  13. # http://www.apache.org/licenses/LICENSE-2.0
  14. #
  15. # Unless required by applicable law or agreed to in writing, software
  16. # distributed under the License is distributed on an "AS IS" BASIS,
  17. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  18. # See the License for the specific language governing permissions and
  19. # limitations under the License.
  20. #
  21. #
  22. # Usage:
  23. #
  24. # pt fetch <id> [-p,--patchset <n>]
  25. # pt checkout <id> [-p,--patchset <n>] [-f,--force]
  26. # pt pull <id> [-p,--patchset <n>]
  27. # pt push [<id>] [-i,--ignore] [-f,--force] [-m,--milestone <milestone>] [-t,--topic <topic>] [-cc <user> <user>]
  28. # pt start <topic> | <id>
  29. # pt propose [new | <branch> | <id>] [-i,--ignore] [-m,--milestone <milestone>] [-t,--topic <topic>] [-cc <user> <user>]
  30. # pt cleanup [<id>]
  31. #
  32. __author__ = 'James Moger'
  33. __version__ = '1.0.6'
  34. import subprocess
  35. import argparse
  36. import errno
  37. import sys
  38. def fetch(args):
  39. """
  40. fetch(args)
  41. Fetches the specified patchset for the ticket from the specified remote.
  42. """
  43. __resolve_remote(args)
  44. # fetch the patchset from the remote repository
  45. if args.patchset is None:
  46. # fetch all current ticket patchsets
  47. print("Fetching ticket patchsets from the '{}' repository".format(args.remote))
  48. if args.quiet:
  49. __call(['git', 'fetch', '-p', args.remote, '--quiet'])
  50. else:
  51. __call(['git', 'fetch', '-p', args.remote])
  52. else:
  53. # fetch specific patchset
  54. __resolve_patchset(args)
  55. print("Fetching ticket {} patchset {} from the '{}' repository".format(args.id, args.patchset, args.remote))
  56. patchset_ref = 'refs/tickets/{:02d}/{:d}/{:d}'.format(args.id % 100, args.id, args.patchset)
  57. if args.quiet:
  58. __call(['git', 'fetch', args.remote, patchset_ref, '--quiet'])
  59. else:
  60. __call(['git', 'fetch', args.remote, patchset_ref])
  61. return
  62. def checkout(args):
  63. """
  64. checkout(args)
  65. Checkout the patchset on a named branch.
  66. """
  67. __resolve_uncommitted_changes_checkout(args)
  68. fetch(args)
  69. # collect local branch names
  70. branches = []
  71. for branch in __call(['git', 'branch']):
  72. if branch[0] == '*':
  73. branches.append(branch[1:].strip())
  74. else:
  75. branches.append(branch.strip())
  76. if args.patchset is None or args.patchset is 0:
  77. branch = 'ticket/{:d}'.format(args.id)
  78. illegals = set(branches) & {'ticket'}
  79. else:
  80. branch = 'patchset/{:d}/{:d}'.format(args.id, args.patchset)
  81. illegals = set(branches) & {'patchset', 'patchset/{:d}'.format(args.id)}
  82. # ensure there are no local branch names that will interfere with branch creation
  83. if len(illegals) > 0:
  84. print('')
  85. print('Sorry, can not complete the checkout for ticket {}.'.format(args.id))
  86. print("The following branches are blocking '{}' branch creation:".format(branch))
  87. for illegal in illegals:
  88. print(' ' + illegal)
  89. exit(errno.EINVAL)
  90. if args.patchset is None or args.patchset is 0:
  91. # checkout the current ticket patchset
  92. if args.force:
  93. __call(['git', 'checkout', '-B', branch, '{}/{}'.format(args.remote, branch)])
  94. else:
  95. __call(['git', 'checkout', branch])
  96. else:
  97. # checkout a specific patchset
  98. __checkout(args.remote, args.id, args.patchset, branch, args.force)
  99. return
  100. def pull(args):
  101. """
  102. pull(args)
  103. Pull (fetch & merge) a ticket patchset into the current branch.
  104. """
  105. __resolve_uncommitted_changes_checkout(args)
  106. __resolve_remote(args)
  107. # reset the checkout before pulling
  108. __call(['git', 'reset', '--hard'])
  109. # pull the patchset from the remote repository
  110. if args.patchset is None or args.patchset is 0:
  111. print("Pulling ticket {} from the '{}' repository".format(args.id, args.remote))
  112. patchset_ref = 'ticket/{:d}'.format(args.id)
  113. else:
  114. __resolve_patchset(args)
  115. print("Pulling ticket {} patchset {} from the '{}' repository".format(args.id, args.patchset, args.remote))
  116. patchset_ref = 'refs/tickets/{:02d}/{:d}/{:d}'.format(args.id % 100, args.id, args.patchset)
  117. if args.squash:
  118. __call(['git', 'pull', '--squash', '--no-log', '--no-rebase', args.remote, patchset_ref], echo=True)
  119. else:
  120. __call(['git', 'pull', '--commit', '--no-ff', '--no-log', '--no-rebase', args.remote, patchset_ref], echo=True)
  121. return
  122. def push(args):
  123. """
  124. push(args)
  125. Push your patchset update or a patchset rewrite.
  126. """
  127. if args.id is None:
  128. # try to determine ticket and patchset from current branch name
  129. for line in __call(['git', 'status', '-b', '-s']):
  130. if line[0:2] == '##':
  131. branch = line[2:].strip()
  132. segments = branch.split('/')
  133. if len(segments) >= 2:
  134. if segments[0] == 'ticket' or segments[0] == 'patchset':
  135. if '...' in segments[1]:
  136. args.id = int(segments[1][:segments[1].index('...')])
  137. else:
  138. args.id = int(segments[1])
  139. args.patchset = None
  140. if args.id is None:
  141. print('Please specify a ticket id for the push command.')
  142. exit(errno.EINVAL)
  143. __resolve_uncommitted_changes_push(args)
  144. __resolve_remote(args)
  145. if args.force:
  146. # rewrite a patchset for an existing ticket
  147. push_ref = 'refs/for/' + str(args.id)
  148. else:
  149. # fast-forward update to an existing patchset
  150. push_ref = 'refs/heads/ticket/{:d}'.format(args.id)
  151. ref_params = __get_pushref_params(args)
  152. ref_spec = 'HEAD:' + push_ref + ref_params
  153. print("Pushing your patchset to the '{}' repository".format(args.remote))
  154. __call(['git', 'push', args.remote, ref_spec], echo=True)
  155. return
  156. def start(args):
  157. """
  158. start(args)
  159. Start development of a topic on a new branch.
  160. """
  161. # collect local branch names
  162. branches = []
  163. for branch in __call(['git', 'branch']):
  164. if branch[0] == '*':
  165. branches.append(branch[1:].strip())
  166. else:
  167. branches.append(branch.strip())
  168. branch = 'topic/' + args.topic
  169. try:
  170. int(args.topic)
  171. branch = 'ticket/' + args.topic
  172. except ValueError:
  173. pass
  174. illegals = set(branches) & {'topic', branch}
  175. # ensure there are no local branch names that will interfere with branch creation
  176. if len(illegals) > 0:
  177. print('Sorry, can not complete the creation of the topic branch.')
  178. print("The following branches are blocking '{}' branch creation:".format(branch))
  179. for illegal in illegals:
  180. print(' ' + illegal)
  181. exit(errno.EINVAL)
  182. __call(['git', 'checkout', '-b', branch])
  183. return
  184. def propose(args):
  185. """
  186. propose_patchset(args)
  187. Push a patchset to create a new proposal ticket or to attach a proposal patchset to an existing ticket.
  188. """
  189. __resolve_uncommitted_changes_push(args)
  190. __resolve_remote(args)
  191. curr_branch = None
  192. push_ref = None
  193. if args.target is None:
  194. # see if the topic is a ticket id
  195. # else default to new
  196. for branch in __call(['git', 'branch']):
  197. if branch[0] == '*':
  198. curr_branch = branch[1:].strip()
  199. if curr_branch.startswith('topic/'):
  200. topic = curr_branch[6:].strip()
  201. try:
  202. int(topic)
  203. push_ref = topic
  204. except ValueError:
  205. pass
  206. if curr_branch.startswith('ticket/'):
  207. topic = curr_branch[7:].strip()
  208. try:
  209. int(topic)
  210. push_ref = topic
  211. except ValueError:
  212. pass
  213. if push_ref is None:
  214. push_ref = 'new'
  215. else:
  216. push_ref = args.target
  217. try:
  218. # check for current patchset and current branch
  219. args.id = int(push_ref)
  220. args.patchset = __get_current_patchset(args.remote, args.id)
  221. if args.patchset > 0:
  222. print('You can not propose a patchset for ticket {} because it already has one.'.format(args.id))
  223. # check current branch for accidental propose instead of push
  224. for line in __call(['git', 'status', '-b', '-s']):
  225. if line[0:2] == '##':
  226. branch = line[2:].strip()
  227. segments = branch.split('/')
  228. if len(segments) >= 2:
  229. if segments[0] == 'ticket':
  230. if '...' in segments[1]:
  231. args.id = int(segments[1][:segments[1].index('...')])
  232. else:
  233. args.id = int(segments[1])
  234. args.patchset = None
  235. print("You are on the '{}' branch, perhaps you meant to push instead?".format(branch))
  236. elif segments[0] == 'patchset':
  237. args.id = int(segments[1])
  238. args.patchset = int(segments[2])
  239. print("You are on the '{}' branch, perhaps you meant to push instead?".format(branch))
  240. exit(errno.EINVAL)
  241. except ValueError:
  242. pass
  243. ref_params = __get_pushref_params(args)
  244. ref_spec = 'HEAD:refs/for/{}{}'.format(push_ref, ref_params)
  245. print("Pushing your proposal to the '{}' repository".format(args.remote))
  246. for line in __call(['git', 'push', args.remote, ref_spec, '-q'], echo=True, err=subprocess.STDOUT):
  247. fields = line.split(':')
  248. if fields[0] == 'remote' and fields[1].strip().startswith('--> #'):
  249. # set the upstream branch configuration
  250. args.id = int(fields[1].strip()[len('--> #'):])
  251. __call(['git', 'fetch', '-p', args.remote])
  252. __call(['git', 'branch', '-u', '{}/ticket/{:d}'.format(args.remote, args.id)])
  253. break
  254. return
  255. def cleanup(args):
  256. """
  257. cleanup(args)
  258. Removes local branches for the ticket.
  259. """
  260. if args.id is None:
  261. branches = __call(['git', 'branch', '--list', 'ticket/*'])
  262. branches += __call(['git', 'branch', '--list', 'patchset/*'])
  263. else:
  264. branches = __call(['git', 'branch', '--list', 'ticket/{:d}'.format(args.id)])
  265. branches += __call(['git', 'branch', '--list', 'patchset/{:d}/*'.format(args.id)])
  266. if len(branches) == 0:
  267. print("No local branches found for ticket {}, cleanup skipped.".format(args.id))
  268. return
  269. if not args.force:
  270. print('Cleanup would remove the following local branches for ticket {}.'.format(args.id))
  271. for branch in branches:
  272. if branch[0] == '*':
  273. print(' ' + branch[1:].strip() + ' (skip)')
  274. else:
  275. print(' ' + branch)
  276. print("To discard these local branches, repeat this command with '--force'.")
  277. exit(errno.EINVAL)
  278. for branch in branches:
  279. if branch[0] == '*':
  280. print('Skipped {} because it is the current branch.'.format(branch[1:].strip()))
  281. continue
  282. __call(['git', 'branch', '-D', branch.strip()], echo=True)
  283. return
  284. def __resolve_uncommitted_changes_checkout(args):
  285. """
  286. __resolve_uncommitted_changes_checkout(args)
  287. Ensures the current checkout has no uncommitted changes that would be discarded by a checkout or pull.
  288. """
  289. status = __call(['git', 'status', '--porcelain'])
  290. for line in status:
  291. if not args.force and line[0] != '?':
  292. print('Your local changes to the following files would be overwritten by {}:'.format(args.command))
  293. print('')
  294. for state in status:
  295. print(state)
  296. print('')
  297. print("To discard your local changes, repeat the {} with '--force'.".format(args.command))
  298. print('NOTE: forcing a {} will HARD RESET your working directory!'.format(args.command))
  299. exit(errno.EINVAL)
  300. def __resolve_uncommitted_changes_push(args):
  301. """
  302. __resolve_uncommitted_changes_push(args)
  303. Ensures the current checkout has no uncommitted changes that should be part of a propose or push.
  304. """
  305. status = __call(['git', 'status', '--porcelain'])
  306. for line in status:
  307. if not args.ignore and line[0] != '?':
  308. print('You have local changes that have not been committed:')
  309. print('')
  310. for state in status:
  311. print(state)
  312. print('')
  313. print("To ignore these uncommitted changes, repeat the {} with '--ignore'.".format(args.command))
  314. exit(errno.EINVAL)
  315. def __resolve_remote(args):
  316. """
  317. __resolve_remote(args)
  318. Identifies the git remote to use for fetching and pushing patchsets by parsing .git/config.
  319. """
  320. remotes = __call(['git', 'remote'])
  321. if len(remotes) == 0:
  322. # no remotes defined
  323. print("Please define a Git remote")
  324. exit(errno.EINVAL)
  325. elif len(remotes) == 1:
  326. # only one remote, use it
  327. args.remote = remotes[0]
  328. return
  329. else:
  330. # multiple remotes, read .git/config
  331. output = __call(['git', 'config', '--local', 'patchsets.remote'], fail=False)
  332. preferred = output[0] if len(output) > 0 else ''
  333. if len(preferred) == 0:
  334. print("You have multiple remote repositories and you have not configured 'patchsets.remote'.")
  335. print("")
  336. print("Available remote repositories:")
  337. for remote in remotes:
  338. print(' ' + remote)
  339. print("")
  340. print("Please set the remote repository to use for patchsets.")
  341. print(" git config --local patchsets.remote <remote>")
  342. exit(errno.EINVAL)
  343. else:
  344. try:
  345. remotes.index(preferred)
  346. except ValueError:
  347. print("The '{}' repository specified in 'patchsets.remote' is not configured!".format(preferred))
  348. print("")
  349. print("Available remotes:")
  350. for remote in remotes:
  351. print(' ' + remote)
  352. print("")
  353. print("Please set the remote repository to use for patchsets.")
  354. print(" git config --local patchsets.remote <remote>")
  355. exit(errno.EINVAL)
  356. args.remote = preferred
  357. return
  358. def __resolve_patchset(args):
  359. """
  360. __resolve_patchset(args)
  361. Resolves the current patchset or validates the the specified patchset exists.
  362. """
  363. if args.patchset is None:
  364. # resolve current patchset
  365. args.patchset = __get_current_patchset(args.remote, args.id)
  366. if args.patchset == 0:
  367. # there are no patchsets for the ticket or the ticket does not exist
  368. print("There are no patchsets for ticket {} in the '{}' repository".format(args.id, args.remote))
  369. exit(errno.EINVAL)
  370. else:
  371. # validate specified patchset
  372. args.patchset = __validate_patchset(args.remote, args.id, args.patchset)
  373. if args.patchset == 0:
  374. # there are no patchsets for the ticket or the ticket does not exist
  375. print("Patchset {} for ticket {} can not be found in the '{}' repository".format(args.patchset, args.id, args.remote))
  376. exit(errno.EINVAL)
  377. return
  378. def __validate_patchset(remote, ticket, patchset):
  379. """
  380. __validate_patchset(remote, ticket, patchset)
  381. Validates that the specified ticket patchset exists.
  382. """
  383. nps = 0
  384. patchset_ref = 'refs/tickets/{:02d}/{:d}/{:d}'.format(ticket % 100, ticket, patchset)
  385. for line in __call(['git', 'ls-remote', remote, patchset_ref]):
  386. ps = int(line.split('/')[4])
  387. if ps > nps:
  388. nps = ps
  389. if nps == patchset:
  390. return patchset
  391. return 0
  392. def __get_current_patchset(remote, ticket):
  393. """
  394. __get_current_patchset(remote, ticket)
  395. Determines the most recent patchset for the ticket by listing the remote patchset refs
  396. for the ticket and parsing the patchset numbers from the resulting set.
  397. """
  398. nps = 0
  399. patchset_refs = 'refs/tickets/{:02d}/{:d}/*'.format(ticket % 100, ticket)
  400. for line in __call(['git', 'ls-remote', remote, patchset_refs]):
  401. ps = int(line.split('/')[4])
  402. if ps > nps:
  403. nps = ps
  404. return nps
  405. def __checkout(remote, ticket, patchset, branch, force=False):
  406. """
  407. __checkout(remote, ticket, patchset, branch)
  408. __checkout(remote, ticket, patchset, branch, force)
  409. Checkout the patchset on a detached head or on a named branch.
  410. """
  411. has_branch = False
  412. on_branch = False
  413. if branch is None or len(branch) == 0:
  414. # checkout the patchset on a detached head
  415. print('Checking out ticket {} patchset {} on a detached HEAD'.format(ticket, patchset))
  416. __call(['git', 'checkout', 'FETCH_HEAD'], echo=True)
  417. return
  418. else:
  419. # checkout on named branch
  420. # determine if we are already on the target branch
  421. for line in __call(['git', 'branch', '--list', branch]):
  422. has_branch = True
  423. if line[0] == '*':
  424. # current branch (* name)
  425. on_branch = True
  426. if not has_branch:
  427. if force:
  428. # force the checkout the patchset to the new named branch
  429. # used when there are local changes to discard
  430. print("Forcing checkout of ticket {} patchset {} on named branch '{}'".format(ticket, patchset, branch))
  431. __call(['git', 'checkout', '-b', branch, 'FETCH_HEAD', '--force'], echo=True)
  432. else:
  433. # checkout the patchset to the new named branch
  434. __call(['git', 'checkout', '-b', branch, 'FETCH_HEAD'], echo=True)
  435. return
  436. if not on_branch:
  437. # switch to existing local branch
  438. __call(['git', 'checkout', branch], echo=True)
  439. #
  440. # now we are on the local branch for the patchset
  441. #
  442. if force:
  443. # reset HEAD to FETCH_HEAD, this drops any local changes
  444. print("Forcing checkout of ticket {} patchset {} on named branch '{}'".format(ticket, patchset, branch))
  445. __call(['git', 'reset', '--hard', 'FETCH_HEAD'], echo=True)
  446. return
  447. else:
  448. # try to merge the existing ref with the FETCH_HEAD
  449. merge = __call(['git', 'merge', '--ff-only', branch, 'FETCH_HEAD'], echo=True, fail=False)
  450. if len(merge) is 1:
  451. up_to_date = merge[0].lower().index('up-to-date') > 0
  452. if up_to_date:
  453. return
  454. elif len(merge) is 0:
  455. print('')
  456. print("Your '{}' branch has diverged from patchset {} on the '{}' repository.".format(branch, patchset, remote))
  457. print('')
  458. print("To discard your local changes, repeat the checkout with '--force'.")
  459. print('NOTE: forcing a checkout will HARD RESET your working directory!')
  460. exit(errno.EINVAL)
  461. return
  462. def __get_pushref_params(args):
  463. """
  464. __get_pushref_params(args)
  465. Returns the push ref parameters for ticket field assignments.
  466. """
  467. params = []
  468. if args.milestone is not None:
  469. params.append('m=' + args.milestone)
  470. if args.topic is not None:
  471. params.append('t=' + args.topic)
  472. else:
  473. for branch in __call(['git', 'branch']):
  474. if branch[0] == '*':
  475. b = branch[1:].strip()
  476. if b.startswith('topic/'):
  477. topic = b[len('topic/'):]
  478. try:
  479. # ignore ticket id topics
  480. int(topic)
  481. except:
  482. # topic is a string
  483. params.append('t=' + topic)
  484. if args.responsible is not None:
  485. params.append('r=' + args.responsible)
  486. if args.cc is not None:
  487. for cc in args.cc:
  488. params.append('cc=' + cc)
  489. if len(params) > 0:
  490. return '%' + ','.join(params)
  491. return ''
  492. def __call(cmd_args, echo=False, fail=True, err=None):
  493. """
  494. __call(cmd_args)
  495. Executes the specified command as a subprocess. The output is parsed and returned as a list
  496. of strings. If the process returns a non-zero exit code, the script terminates with that
  497. exit code. Std err of the subprocess is passed-through to the std err of the parent process.
  498. """
  499. p = subprocess.Popen(cmd_args, stdout=subprocess.PIPE, stderr=err, universal_newlines=True)
  500. lines = []
  501. for line in iter(p.stdout.readline, b''):
  502. line_str = str(line).strip()
  503. if len(line_str) is 0:
  504. break
  505. lines.append(line_str)
  506. if echo:
  507. print(line_str)
  508. p.wait()
  509. if fail and p.returncode is not 0:
  510. exit(p.returncode)
  511. return lines
  512. #
  513. # define the acceptable arguments and their usage/descriptions
  514. #
  515. # force argument
  516. force_arg = argparse.ArgumentParser(add_help=False)
  517. force_arg.add_argument('-f', '--force', default=False, help='force the command to complete', action='store_true')
  518. # quiet argument
  519. quiet_arg = argparse.ArgumentParser(add_help=False)
  520. quiet_arg.add_argument('-q', '--quiet', default=False, help='suppress git stderr output', action='store_true')
  521. # ticket & patchset arguments
  522. ticket_args = argparse.ArgumentParser(add_help=False)
  523. ticket_args.add_argument('id', help='the ticket id', type=int)
  524. ticket_args.add_argument('-p', '--patchset', help='the patchset number', type=int)
  525. # push refspec arguments
  526. push_args = argparse.ArgumentParser(add_help=False)
  527. push_args.add_argument('-i', '--ignore', default=False, help='ignore uncommitted changes', action='store_true')
  528. push_args.add_argument('-m', '--milestone', help='set the milestone')
  529. push_args.add_argument('-r', '--responsible', help='set the responsible user')
  530. push_args.add_argument('-t', '--topic', help='set the topic')
  531. push_args.add_argument('-cc', nargs='+', help='specify accounts to add to the watch list')
  532. # the commands
  533. parser = argparse.ArgumentParser(description='a Patchset Tool for Gitblit Tickets')
  534. parser.add_argument('--version', action='version', version='%(prog)s {}'.format(__version__))
  535. commands = parser.add_subparsers(dest='command', title='commands')
  536. fetch_parser = commands.add_parser('fetch', help='fetch a patchset', parents=[ticket_args, quiet_arg])
  537. fetch_parser.set_defaults(func=fetch)
  538. checkout_parser = commands.add_parser('checkout', aliases=['co'],
  539. help='fetch & checkout a patchset to a branch',
  540. parents=[ticket_args, force_arg, quiet_arg])
  541. checkout_parser.set_defaults(func=checkout)
  542. pull_parser = commands.add_parser('pull',
  543. help='fetch & merge a patchset into the current branch',
  544. parents=[ticket_args, force_arg])
  545. pull_parser.add_argument('-s', '--squash',
  546. help='squash the pulled patchset into your working directory',
  547. default=False,
  548. action='store_true')
  549. pull_parser.set_defaults(func=pull)
  550. push_parser = commands.add_parser('push', aliases=['up'],
  551. help='upload your patchset changes',
  552. parents=[push_args, force_arg])
  553. push_parser.add_argument('id', help='the ticket id', nargs='?', type=int)
  554. push_parser.set_defaults(func=push)
  555. propose_parser = commands.add_parser('propose', help='propose a new ticket or the first patchset', parents=[push_args])
  556. propose_parser.add_argument('target', help="the ticket id, 'new', or the integration branch", nargs='?')
  557. propose_parser.set_defaults(func=propose)
  558. cleanup_parser = commands.add_parser('cleanup', aliases=['rm'],
  559. help='remove local ticket branches',
  560. parents=[force_arg])
  561. cleanup_parser.add_argument('id', help='the ticket id', nargs='?', type=int)
  562. cleanup_parser.set_defaults(func=cleanup)
  563. start_parser = commands.add_parser('start', help='start a new branch for the topic or ticket')
  564. start_parser.add_argument('topic', help="the topic or ticket id")
  565. start_parser.set_defaults(func=start)
  566. if len(sys.argv) < 2:
  567. parser.parse_args(['--help'])
  568. else:
  569. # parse the command-line arguments
  570. script_args = parser.parse_args()
  571. # exec the specified command
  572. script_args.func(script_args)