def remove_empty_branches(branch_tree): tag_set = git.tags() ensure_root_checkout = git.once(lambda: git.run("checkout", git.root())) deletions = set() downstreams = collections.defaultdict(list) for branch, parent in git.topo_iter(branch_tree, top_down=False): downstreams[parent].append(branch) if git.hash_one(branch) == git.hash_one(parent): ensure_root_checkout() logging.debug("branch %s merged to %s", branch, parent) for down in downstreams[branch]: if down in deletions: continue if parent in tag_set: git.set_branch_config(down, "remote", ".") git.set_branch_config(down, "merge", "refs/tags/%s" % parent) print ("Reparented %s to track %s [tag] (was tracking %s)" % (down, parent, branch)) else: git.run("branch", "--set-upstream-to", parent, down) print ("Reparented %s to track %s (was tracking %s)" % (down, parent, branch)) deletions.add(branch) print git.run("branch", "-d", branch)
def remove_empty_branches(branch_tree): tag_set = git.tags() ensure_root_checkout = git.once(lambda: git.run('checkout', git.root())) deletions = set() downstreams = collections.defaultdict(list) for branch, parent in git.topo_iter(branch_tree, top_down=False): downstreams[parent].append(branch) if git.hash_one(branch) == git.hash_one(parent): ensure_root_checkout() logging.debug('branch %s merged to %s', branch, parent) for down in downstreams[branch]: if down in deletions: continue if parent in tag_set: git.set_branch_config(down, 'remote', '.') git.set_branch_config(down, 'merge', 'refs/tags/%s' % parent) print('Reparented %s to track %s [tag] (was tracking %s)' % (down, parent, branch)) else: git.run('branch', '--set-upstream-to', parent, down) print('Reparented %s to track %s (was tracking %s)' % (down, parent, branch)) deletions.add(branch) print git.run('branch', '-d', branch)
def remove_empty_branches(branch_tree): tag_set = git.tags() ensure_root_checkout = git.once(lambda: git.run('checkout', git.root())) deletions = {} reparents = {} downstreams = collections.defaultdict(list) for branch, parent in git.topo_iter(branch_tree, top_down=False): if git.is_dormant(branch): continue downstreams[parent].append(branch) # If branch and parent have the same tree, then branch has to be marked # for deletion and its children and grand-children reparented to parent. if git.hash_one(branch + ":") == git.hash_one(parent + ":"): ensure_root_checkout() logging.debug('branch %s merged to %s', branch, parent) # Mark branch for deletion while remembering the ordering, then add all # its children as grand-children of its parent and record reparenting # information if necessary. deletions[branch] = len(deletions) for down in downstreams[branch]: if down in deletions: continue # Record the new and old parent for down, or update such a record # if it already exists. Keep track of the ordering so that reparenting # happen in topological order. downstreams[parent].append(down) if down not in reparents: reparents[down] = (len(reparents), parent, branch) else: order, _, old_parent = reparents[down] reparents[down] = (order, parent, old_parent) # Apply all reparenting recorded, in order. for branch, value in sorted(reparents.items(), key=lambda x: x[1][0]): _, parent, old_parent = value if parent in tag_set: git.set_branch_config(branch, 'remote', '.') git.set_branch_config(branch, 'merge', 'refs/tags/%s' % parent) print('Reparented %s to track %s [tag] (was tracking %s)' % (branch, parent, old_parent)) else: git.run('branch', '--set-upstream-to', parent, branch) print('Reparented %s to track %s (was tracking %s)' % (branch, parent, old_parent)) # Apply all deletions recorded, in order. for branch, _ in sorted(deletions.items(), key=lambda x: x[1]): print(git.run('branch', '-d', branch))
def remove_empty_branches(branch_tree): tag_set = git.tags() ensure_root_checkout = git.once(lambda: git.run('checkout', git.root())) deletions = {} reparents = {} downstreams = collections.defaultdict(list) for branch, parent in git.topo_iter(branch_tree, top_down=False): downstreams[parent].append(branch) # If branch and parent have the same tree, then branch has to be marked # for deletion and its children and grand-children reparented to parent. if git.hash_one(branch+":") == git.hash_one(parent+":"): ensure_root_checkout() logging.debug('branch %s merged to %s', branch, parent) # Mark branch for deletion while remembering the ordering, then add all # its children as grand-children of its parent and record reparenting # information if necessary. deletions[branch] = len(deletions) for down in downstreams[branch]: if down in deletions: continue # Record the new and old parent for down, or update such a record # if it already exists. Keep track of the ordering so that reparenting # happen in topological order. downstreams[parent].append(down) if down not in reparents: reparents[down] = (len(reparents), parent, branch) else: order, _, old_parent = reparents[down] reparents[down] = (order, parent, old_parent) # Apply all reparenting recorded, in order. for branch, value in sorted(reparents.iteritems(), key=lambda x:x[1][0]): _, parent, old_parent = value if parent in tag_set: git.set_branch_config(branch, 'remote', '.') git.set_branch_config(branch, 'merge', 'refs/tags/%s' % parent) print ('Reparented %s to track %s [tag] (was tracking %s)' % (branch, parent, old_parent)) else: git.run('branch', '--set-upstream-to', parent, branch) print ('Reparented %s to track %s (was tracking %s)' % (branch, parent, old_parent)) # Apply all deletions recorded, in order. for branch, _ in sorted(deletions.iteritems(), key=lambda x: x[1]): print git.run('branch', '-d', branch)
def main(args): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter ) parser.add_argument('branch_name') g = parser.add_mutually_exclusive_group() g.add_argument('--upstream_current', action='store_true', help='set upstream branch to current branch.') g.add_argument('--upstream', metavar='REF', default=root(), help='upstream branch (or tag) to track.') g.add_argument('--lkgr', action='store_const', const='lkgr', dest='upstream', help='set basis ref for new branch to lkgr.') opts = parser.parse_args(args) try: if opts.upstream_current: run('checkout', '--track', '-b', opts.branch_name) else: if opts.upstream in tags(): # TODO(iannucci): ensure that basis_ref is an ancestor of HEAD? run('checkout', '--no-track', '-b', opts.branch_name, hash_one(opts.upstream)) set_config('branch.%s.remote' % opts.branch_name, '.') set_config('branch.%s.merge' % opts.branch_name, opts.upstream) else: # TODO(iannucci): Detect unclean workdir then stash+pop if we need to # teleport to a conflicting portion of history? run('checkout', '--track', opts.upstream, '-b', opts.branch_name) get_or_create_merge_base(opts.branch_name) except subprocess2.CalledProcessError as cpe: sys.stdout.write(cpe.stdout) sys.stderr.write(cpe.stderr) return 1
def start(self): self.__branches_info = get_branches_info( include_tracking_status=self.verbosity >= 1) roots = set() # A map of parents to a list of their children. for branch, branch_info in self.__branches_info.iteritems(): if not branch_info: continue parent = branch_info.upstream if parent and not self.__branches_info[parent]: branch_upstream = upstream(branch) # If git can't find the upstream, mark the upstream as gone. if branch_upstream: parent = branch_upstream else: self.__gone_branches.add(parent) # A parent that isn't in the branches info is a root. roots.add(parent) self.__parent_map[parent].append(branch) self.__current_branch = current_branch() self.__current_hash = hash_one('HEAD', short=True) self.__tag_set = tags() if roots: for root in sorted(roots): self.__append_branch(root) else: no_branches = OutputLine() no_branches.append('No User Branches') self.output.append(no_branches)
def start(self): self.__branches_info = get_branches_info( include_tracking_status=self.verbosity >= 1) roots = set() # A map of parents to a list of their children. for branch, branch_info in self.__branches_info.iteritems(): if not branch_info: continue parent = branch_info.upstream if not self.__branches_info[parent]: branch_upstream = upstream(branch) # If git can't find the upstream, mark the upstream as gone. if branch_upstream: parent = branch_upstream else: self.__gone_branches.add(parent) # A parent that isn't in the branches info is a root. roots.add(parent) self.__parent_map[parent].append(branch) self.__current_branch = current_branch() self.__current_hash = hash_one('HEAD', short=True) self.__tag_set = tags() if roots: for root in sorted(roots): self.__append_branch(root) else: no_branches = OutputLine() no_branches.append('No User Branches') self.output.append(no_branches)
def main(argv): assert len(argv) == 1, "No arguments expected" upfn = upstream cur = current_branch() if cur == 'HEAD': def _upfn(b): parent = upstream(b) if parent: return hash_one(parent) upfn = _upfn cur = hash_one(cur) downstreams = [b for b in branches() if upfn(b) == cur] if not downstreams: return "No downstream branches" elif len(downstreams) == 1: run('checkout', downstreams[0]) else: high = len(downstreams) - 1 print while True: print "Please select a downstream branch" for i, b in enumerate(downstreams): print " %d. %s" % (i, b) r = raw_input("Selection (0-%d)[0]: " % high).strip() or '0' if not r.isdigit() or (0 > int(r) > high): print "Invalid choice." else: run('checkout', downstreams[int(r)]) break
def main(argv): colorama.init() assert len(argv) == 1, "No arguments expected" branch_map = {} par_map = collections.defaultdict(list) for branch in branches(): par = upstream(branch) or NO_UPSTREAM branch_map[branch] = par par_map[par].append(branch) current = current_branch() hashes = hash_multi(current, *branch_map.keys()) current_hash = hashes[0] par_hashes = { k: hashes[i + 1] for i, k in enumerate(branch_map.iterkeys()) } par_hashes[NO_UPSTREAM] = 0 tag_set = tags() while par_map: for parent in par_map: if parent not in branch_map: if parent not in par_hashes: par_hashes[parent] = hash_one(parent) print_branch(current, current_hash, parent, par_hashes, par_map, branch_map, tag_set) break
def main(args): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=__doc__, ) parser.add_argument('branch_name') g = parser.add_mutually_exclusive_group() g.add_argument('--upstream-current', '--upstream_current', action='store_true', help='set upstream branch to current branch.') g.add_argument('--upstream', metavar='REF', default=root(), help='upstream branch (or tag) to track.') g.add_argument('--inject-current', '--inject_current', action='store_true', help='new branch adopts current branch\'s upstream,' + ' and new branch becomes current branch\'s upstream.') g.add_argument('--lkgr', action='store_const', const='lkgr', dest='upstream', help='set basis ref for new branch to lkgr.') opts = parser.parse_args(args) try: if opts.inject_current: below = current_branch() if below is None: raise Exception('no current branch') above = upstream(below) if above is None: raise Exception('branch %s has no upstream' % (below)) run('checkout', '--track', above, '-b', opts.branch_name) run('branch', '--set-upstream-to', opts.branch_name, below) elif opts.upstream_current: run('checkout', '--track', '-b', opts.branch_name) else: if opts.upstream in tags(): # TODO(iannucci): ensure that basis_ref is an ancestor of HEAD? run('checkout', '--no-track', '-b', opts.branch_name, hash_one(opts.upstream)) set_config('branch.%s.remote' % opts.branch_name, '.') set_config('branch.%s.merge' % opts.branch_name, opts.upstream) else: # TODO(iannucci): Detect unclean workdir then stash+pop if we need to # teleport to a conflicting portion of history? run('checkout', '--track', opts.upstream, '-b', opts.branch_name) get_or_create_merge_base(opts.branch_name) except subprocess2.CalledProcessError as cpe: sys.stdout.write(cpe.stdout) sys.stderr.write(cpe.stderr) return 1 sys.stderr.write('Switched to branch %s.\n' % opts.branch_name) return 0
def start(self): self.__branches_info = get_branches_info( include_tracking_status=self.verbosity >= 1) if (self.verbosity >= 2): # Avoid heavy import unless necessary. from git_cl import get_cl_statuses, color_for_status, Changelist change_cls = [ Changelist(branchref='refs/heads/' + b) for b in self.__branches_info.keys() if b ] status_info = get_cl_statuses(change_cls, fine_grained=self.verbosity > 2, max_processes=self.maxjobs) # This is a blocking get which waits for the remote CL status to be # retrieved. for cl, status in status_info: self.__status_info[cl.GetBranch()] = (cl.GetIssueURL(), color_for_status(status), status) roots = set() # A map of parents to a list of their children. for branch, branch_info in self.__branches_info.items(): if not branch_info: continue parent = branch_info.upstream if self.__check_cycle(branch): continue if not self.__branches_info[parent]: branch_upstream = upstream(branch) # If git can't find the upstream, mark the upstream as gone. if branch_upstream: parent = branch_upstream else: self.__gone_branches.add(parent) # A parent that isn't in the branches info is a root. roots.add(parent) self.__parent_map[parent].append(branch) self.__current_branch = current_branch() self.__current_hash = hash_one('HEAD', short=True) self.__tag_set = tags() if roots: for root in sorted(roots): self.__append_branch(root) else: no_branches = OutputLine() no_branches.append('No User Branches') self.output.append(no_branches)
def start(self): self.__branches_info = get_branches_info( include_tracking_status=self.verbosity >= 1) if (self.verbosity >= 2): # Avoid heavy import unless necessary. from git_cl import get_cl_statuses, color_for_status, Changelist change_cls = [Changelist(branchref='refs/heads/'+b) for b in self.__branches_info.keys() if b] status_info = get_cl_statuses(change_cls, fine_grained=self.verbosity > 2, max_processes=self.maxjobs) # This is a blocking get which waits for the remote CL status to be # retrieved. for cl, status in status_info: self.__status_info[cl.GetBranch()] = (cl.GetIssueURL(), color_for_status(status), status) roots = set() # A map of parents to a list of their children. for branch, branch_info in self.__branches_info.iteritems(): if not branch_info: continue parent = branch_info.upstream if not self.__branches_info[parent]: branch_upstream = upstream(branch) # If git can't find the upstream, mark the upstream as gone. if branch_upstream: parent = branch_upstream else: self.__gone_branches.add(parent) # A parent that isn't in the branches info is a root. roots.add(parent) self.__parent_map[parent].append(branch) self.__current_branch = current_branch() self.__current_hash = hash_one('HEAD', short=True) self.__tag_set = tags() if roots: for root in sorted(roots): self.__append_branch(root) else: no_branches = OutputLine() no_branches.append('No User Branches') self.output.append(no_branches)
def main(args, stdout=sys.stdout, stderr=sys.stderr): parser = argparse.ArgumentParser( prog='git hyper-blame', description='git blame with support for ignoring certain commits.') parser.add_argument('-i', metavar='REVISION', action='append', dest='ignored', default=[], help='a revision to ignore') parser.add_argument('--ignore-file', metavar='FILE', type=argparse.FileType('r'), dest='ignore_file', help='a file containing a list of revisions to ignore') parser.add_argument('--no-default-ignores', dest='no_default_ignores', action='store_true', help='Do not ignore commits from .git-blame-ignore-revs.') parser.add_argument('revision', nargs='?', default='HEAD', metavar='REVISION', help='revision to look at') parser.add_argument('filename', metavar='FILE', help='filename to blame') args = parser.parse_args(args) try: repo_root = git_common.repo_root() except subprocess2.CalledProcessError as e: stderr.write(e.stderr) return e.returncode # Make filename relative to the repository root, and cd to the root dir (so # all filenames throughout this script are relative to the root). filename = os.path.relpath(args.filename, repo_root) os.chdir(repo_root) # Normalize filename so we can compare it to other filenames git gives us. filename = os.path.normpath(filename) filename = os.path.normcase(filename) ignored_list = list(args.ignored) if not args.no_default_ignores and os.path.exists(DEFAULT_IGNORE_FILE_NAME): with open(DEFAULT_IGNORE_FILE_NAME) as ignore_file: ignored_list.extend(parse_ignore_file(ignore_file)) if args.ignore_file: ignored_list.extend(parse_ignore_file(args.ignore_file)) ignored = set() for c in ignored_list: try: ignored.add(git_common.hash_one(c)) except subprocess2.CalledProcessError as e: # Custom warning string (the message from git-rev-parse is inappropriate). stderr.write('warning: unknown revision \'%s\'.\n' % c) return hyper_blame(ignored, filename, args.revision, out=stdout, err=stderr)
def main(args, stdout=sys.stdout, stderr=sys.stderr): parser = argparse.ArgumentParser( prog='git hyper-blame', description='git blame with support for ignoring certain commits.') parser.add_argument('-i', metavar='REVISION', action='append', dest='ignored', default=[], help='a revision to ignore') parser.add_argument('--ignore-file', metavar='FILE', type=argparse.FileType('r'), dest='ignore_file', help='a file containing a list of revisions to ignore') parser.add_argument('--no-default-ignores', dest='no_default_ignores', action='store_true', help='Do not ignore commits from .git-blame-ignore-revs.') parser.add_argument('revision', nargs='?', default='HEAD', metavar='REVISION', help='revision to look at') parser.add_argument('filename', metavar='FILE', help='filename to blame') args = parser.parse_args(args) try: repo_root = git_common.repo_root() except subprocess2.CalledProcessError as e: stderr.write(e.stderr.decode()) return e.returncode # Make filename relative to the repository root, and cd to the root dir (so # all filenames throughout this script are relative to the root). filename = os.path.relpath(args.filename, repo_root) os.chdir(repo_root) # Normalize filename so we can compare it to other filenames git gives us. filename = os.path.normpath(filename) filename = os.path.normcase(filename) ignored_list = list(args.ignored) if not args.no_default_ignores and os.path.exists(DEFAULT_IGNORE_FILE_NAME): with open(DEFAULT_IGNORE_FILE_NAME) as ignore_file: ignored_list.extend(parse_ignore_file(ignore_file)) if args.ignore_file: ignored_list.extend(parse_ignore_file(args.ignore_file)) ignored = set() for c in ignored_list: try: ignored.add(git_common.hash_one(c)) except subprocess2.CalledProcessError as e: # Custom warning string (the message from git-rev-parse is inappropriate). stderr.write('warning: unknown revision \'%s\'.\n' % c) return hyper_blame(ignored, filename, args.revision, out=stdout, err=stderr)
def start(self): self.__branches_info = get_branches_info( include_tracking_status=self.verbosity >= 1) if (self.verbosity >= 2): # Avoid heavy import unless necessary. from git_cl import get_cl_statuses status_info = get_cl_statuses(self.__branches_info.keys(), fine_grained=self.verbosity > 2, max_processes=self.maxjobs) for _ in xrange(len(self.__branches_info)): # This is a blocking get which waits for the remote CL status to be # retrieved. (branch, url, color) = status_info.next() self.__status_info[branch] = (url, color) roots = set() # A map of parents to a list of their children. for branch, branch_info in self.__branches_info.iteritems(): if not branch_info: continue parent = branch_info.upstream if not self.__branches_info[parent]: branch_upstream = upstream(branch) # If git can't find the upstream, mark the upstream as gone. if branch_upstream: parent = branch_upstream else: self.__gone_branches.add(parent) # A parent that isn't in the branches info is a root. roots.add(parent) self.__parent_map[parent].append(branch) self.__current_branch = current_branch() self.__current_hash = hash_one('HEAD', short=True) self.__tag_set = tags() if roots: for root in sorted(roots): self.__append_branch(root) else: no_branches = OutputLine() no_branches.append('No User Branches') self.output.append(no_branches)
def main(args): parser = argparse.ArgumentParser() parser.add_argument('--pick', help=('The number to pick if this command would ' 'prompt')) opts = parser.parse_args(args) upfn = upstream cur = current_branch() if cur == 'HEAD': def _upfn(b): parent = upstream(b) if parent: return hash_one(parent) upfn = _upfn cur = hash_one(cur) downstreams = [b for b in branches() if upfn(b) == cur] if not downstreams: return "No downstream branches" elif len(downstreams) == 1: run_stream('checkout', downstreams[0], stdout=sys.stdout, stderr=sys.stderr) else: high = len(downstreams) - 1 while True: print "Please select a downstream branch" for i, b in enumerate(downstreams): print " %d. %s" % (i, b) prompt = "Selection (0-%d)[0]: " % high r = opts.pick if r: print prompt + r else: r = raw_input(prompt).strip() or '0' if not r.isdigit() or (0 > int(r) > high): print "Invalid choice." else: run_stream('checkout', downstreams[int(r)], stdout=sys.stdout, stderr=sys.stderr) break
def main(argv): parser = argparse.ArgumentParser( description=__doc__.strip().splitlines()[0], epilog=' '.join(__doc__.strip().splitlines()[1:])) g = parser.add_mutually_exclusive_group() g.add_argument( 'merge_base', nargs='?', help='The new hash to use as the merge base for the current branch') g.add_argument('--delete', '-d', action='store_true', help='Remove the set mark.') opts = parser.parse_args(argv) cur = current_branch() if opts.delete: try: remove_merge_base(cur) except CalledProcessError: print "No merge base currently exists for %s." % cur return 0 if opts.merge_base: try: opts.merge_base = hash_one(opts.merge_base) except CalledProcessError: print >> sys.stderr, ('fatal: could not resolve %s as a commit' % (opts.merge_base)) return 1 manual_merge_base(cur, opts.merge_base, upstream(cur)) ret = 0 actual = get_or_create_merge_base(cur) if opts.merge_base and opts.merge_base != actual: ret = 1 print "Invalid merge_base %s" % opts.merge_base print "merge_base(%s): %s" % (cur, actual) return ret
def main(argv): parser = argparse.ArgumentParser( description=__doc__.strip().splitlines()[0], epilog=' '.join(__doc__.strip().splitlines()[1:])) g = parser.add_mutually_exclusive_group() g.add_argument( 'merge_base', nargs='?', help='The new hash to use as the merge base for the current branch' ) g.add_argument('--delete', '-d', action='store_true', help='Remove the set mark.') opts = parser.parse_args(argv) cur = current_branch() if opts.delete: try: remove_merge_base(cur) except CalledProcessError: print "No merge base currently exists for %s." % cur return 0 if opts.merge_base: try: opts.merge_base = hash_one(opts.merge_base) except CalledProcessError: print >> sys.stderr, ( 'fatal: could not resolve %s as a commit' % (opts.merge_base) ) return 1 manual_merge_base(cur, opts.merge_base) ret = 0 actual = get_or_create_merge_base(cur) if opts.merge_base and opts.merge_base != actual: ret = 1 print "Invalid merge_base %s" % opts.merge_base print "merge_base(%s): %s" % (cur, actual) return ret
def main(args): parser = argparse.ArgumentParser() parser.add_argument('--pick', help=( 'The number to pick if this command would ' 'prompt')) opts = parser.parse_args(args) upfn = upstream cur = current_branch() if cur == 'HEAD': def _upfn(b): parent = upstream(b) if parent: return hash_one(parent) upfn = _upfn cur = hash_one(cur) downstreams = [b for b in branches() if upfn(b) == cur] if not downstreams: print "No downstream branches" return 1 elif len(downstreams) == 1: run('checkout', downstreams[0], stdout=sys.stdout, stderr=sys.stderr) else: high = len(downstreams) - 1 while True: print "Please select a downstream branch" for i, b in enumerate(downstreams): print " %d. %s" % (i, b) prompt = "Selection (0-%d)[0]: " % high r = opts.pick if r: print prompt + r else: r = raw_input(prompt).strip() or '0' if not r.isdigit() or (0 > int(r) > high): print "Invalid choice." else: run('checkout', downstreams[int(r)], stdout=sys.stdout, stderr=sys.stderr) break return 0
def main(args): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('branch_name') g = parser.add_mutually_exclusive_group() g.add_argument('--upstream_current', action='store_true', help='set upstream branch to current branch.') g.add_argument('--upstream', metavar='REF', default=root(), help='upstream branch (or tag) to track.') g.add_argument('--lkgr', action='store_const', const='lkgr', dest='upstream', help='set basis ref for new branch to lkgr.') opts = parser.parse_args(args) try: if opts.upstream_current: run('checkout', '--track', '-b', opts.branch_name) else: if opts.upstream in tags(): # TODO(iannucci): ensure that basis_ref is an ancestor of HEAD? run('checkout', '--no-track', '-b', opts.branch_name, hash_one(opts.upstream)) set_config('branch.%s.remote' % opts.branch_name, '.') set_config('branch.%s.merge' % opts.branch_name, opts.upstream) else: # TODO(iannucci): Detect unclean workdir then stash+pop if we need to # teleport to a conflicting portion of history? run('checkout', '--track', opts.upstream, '-b', opts.branch_name) get_or_create_merge_base(opts.branch_name) except subprocess2.CalledProcessError as cpe: sys.stdout.write(cpe.stdout) sys.stderr.write(cpe.stderr) return 1
def create_new_branch(branch_name, upstream_current=False, upstream=None, inject_current=False): upstream = upstream or git_common.root() try: if inject_current: below = git_common.current_branch() if below is None: raise Exception('no current branch') above = git_common.upstream(below) if above is None: raise Exception('branch %s has no upstream' % (below)) git_common.run('checkout', '--track', above, '-b', branch_name) git_common.run('branch', '--set-upstream-to', branch_name, below) elif upstream_current: git_common.run('checkout', '--track', '-b', branch_name) else: if upstream in git_common.tags(): # TODO(iannucci): ensure that basis_ref is an ancestor of HEAD? git_common.run('checkout', '--no-track', '-b', branch_name, git_common.hash_one(upstream)) git_common.set_config('branch.%s.remote' % branch_name, '.') git_common.set_config('branch.%s.merge' % branch_name, upstream) else: # TODO(iannucci): Detect unclean workdir then stash+pop if we need to # teleport to a conflicting portion of history? git_common.run('checkout', '--track', upstream, '-b', branch_name) git_common.get_or_create_merge_base(branch_name) except subprocess2.CalledProcessError as cpe: sys.stdout.write(cpe.stdout.decode('utf-8', 'replace')) sys.stderr.write(cpe.stderr.decode('utf-8', 'replace')) return 1 sys.stderr.write('Switched to branch %s.\n' % branch_name) return 0
def main(args, stdout=sys.stdout, stderr=sys.stderr): parser = argparse.ArgumentParser( prog='git hyper-blame', description='git blame with support for ignoring certain commits.') parser.add_argument('-i', metavar='REVISION', action='append', dest='ignored', default=[], help='a revision to ignore') parser.add_argument('revision', nargs='?', default='HEAD', metavar='REVISION', help='revision to look at') parser.add_argument('filename', metavar='FILE', help='filename to blame') args = parser.parse_args(args) try: repo_root = git_common.repo_root() except subprocess2.CalledProcessError as e: stderr.write(e.stderr) return e.returncode # Make filename relative to the repository root, and cd to the root dir (so # all filenames throughout this script are relative to the root). filename = os.path.relpath(args.filename, repo_root) os.chdir(repo_root) # Normalize filename so we can compare it to other filenames git gives us. filename = os.path.normpath(filename) filename = os.path.normcase(filename) ignored = set() for c in args.ignored: try: ignored.add(git_common.hash_one(c)) except subprocess2.CalledProcessError as e: # Custom error message (the message from git-rev-parse is inappropriate). stderr.write('fatal: unknown revision \'%s\'.\n' % c) return e.returncode return hyper_blame(ignored, filename, args.revision, out=stdout, err=stderr)
def main(argv): colorama.init() assert len(argv) == 1, "No arguments expected" branch_map = {} par_map = collections.defaultdict(list) for branch in branches(): par = upstream(branch) or NO_UPSTREAM branch_map[branch] = par par_map[par].append(branch) current = current_branch() hashes = hash_multi(current, *branch_map.keys()) current_hash = hashes[0] par_hashes = {k: hashes[i+1] for i, k in enumerate(branch_map.iterkeys())} par_hashes[NO_UPSTREAM] = 0 tag_set = tags() while par_map: for parent in par_map: if parent not in branch_map: if parent not in par_hashes: par_hashes[parent] = hash_one(parent) print_branch(current, current_hash, parent, par_hashes, par_map, branch_map, tag_set) break
def __append_branch(self, branch, depth=0): """Recurses through the tree structure and appends an OutputLine to the OutputManager for each branch.""" branch_info = self.__branches_info[branch] if branch_info: branch_hash = branch_info.hash else: try: branch_hash = hash_one(branch, short=True) except subprocess2.CalledProcessError: branch_hash = None line = OutputLine() # The branch name with appropriate indentation. suffix = '' if branch == self.__current_branch or ( self.__current_branch == 'HEAD' and branch == self.__current_hash): suffix = ' *' branch_string = branch if branch in self.__gone_branches: branch_string = '{%s:GONE}' % branch if not branch: branch_string = '{NO_UPSTREAM}' main_string = ' ' * depth + branch_string + suffix line.append( main_string, color=self.__color_for_branch(branch, branch_hash)) # The branch hash. if self.verbosity >= 2: line.append(branch_hash or '', separator=' ', color=Fore.RED) # The branch tracking status. if self.verbosity >= 1: ahead_string = '' behind_string = '' front_separator = '' center_separator = '' back_separator = '' if branch_info and not self.__is_invalid_parent(branch_info.upstream): ahead = branch_info.ahead behind = branch_info.behind if ahead: ahead_string = 'ahead %d' % ahead if behind: behind_string = 'behind %d' % behind if ahead or behind: front_separator = '[' back_separator = ']' if ahead and behind: center_separator = '|' line.append(front_separator, separator=' ') line.append(ahead_string, separator=' ', color=Fore.MAGENTA) line.append(center_separator, separator=' ') line.append(behind_string, separator=' ', color=Fore.MAGENTA) line.append(back_separator) # The Rietveld issue associated with the branch. if self.verbosity >= 2: (url, color, status) = ('', '', '') if self.__is_invalid_parent(branch) \ else self.__status_info[branch] if self.verbosity > 2: line.append('{} ({})'.format(url, status) if url else '', color=color) else: line.append(url or '', color=color) # The subject of the most recent commit on the branch. if self.show_subject: line.append(run('log', '-n1', '--format=%s', branch, '--')) self.output.append(line) for child in sorted(self.__parent_map.pop(branch, ())): self.__append_branch(child, depth=depth + 1)
def rebase_branch(branch, parent, start_hash): logging.debug('considering %s(%s) -> %s(%s) : %s', branch, git.hash_one(branch), parent, git.hash_one(parent), start_hash) # If parent has FROZEN commits, don't base branch on top of them. Instead, # base branch on top of whatever commit is before them. back_ups = 0 orig_parent = parent while git.run('log', '-n1', '--format=%s', parent, '--').startswith(git.FREEZE): back_ups += 1 parent = git.run('rev-parse', parent+'~') if back_ups: logging.debug('Backed parent up by %d from %s to %s', back_ups, orig_parent, parent) if git.hash_one(parent) != start_hash: # Try a plain rebase first print 'Rebasing:', branch rebase_ret = git.rebase(parent, start_hash, branch, abort=True) if not rebase_ret.success: # TODO(iannucci): Find collapsible branches in a smarter way? print "Failed! Attempting to squash", branch, "...", squash_branch = branch+"_squash_attempt" git.run('checkout', '-b', squash_branch) git.squash_current_branch(merge_base=start_hash) # Try to rebase the branch_squash_attempt branch to see if it's empty. squash_ret = git.rebase(parent, start_hash, squash_branch, abort=True) empty_rebase = git.hash_one(squash_branch) == git.hash_one(parent) git.run('checkout', branch) git.run('branch', '-D', squash_branch) if squash_ret.success and empty_rebase: print 'Success!' git.squash_current_branch(merge_base=start_hash) git.rebase(parent, start_hash, branch) else: print "Failed!" print # rebase and leave in mid-rebase state. # This second rebase attempt should always fail in the same # way that the first one does. If it magically succeeds then # something very strange has happened. second_rebase_ret = git.rebase(parent, start_hash, branch) if second_rebase_ret.success: # pragma: no cover print "Second rebase succeeded unexpectedly!" print "Please see: http://crbug.com/425696" print "First rebased failed with:" print rebase_ret.stderr else: print "Here's what git-rebase (squashed) had to say:" print print squash_ret.stdout print squash_ret.stderr print textwrap.dedent( """\ Squashing failed. You probably have a real merge conflict. Your working copy is in mid-rebase. Either: * completely resolve like a normal git-rebase; OR * abort the rebase and mark this branch as dormant: git config branch.%s.dormant true And then run `git rebase-update` again to resume. """ % branch) return False else: print '%s up-to-date' % branch git.remove_merge_base(branch) git.get_or_create_merge_base(branch) return True
def __append_branch(self, branch, depth=0): """Recurses through the tree structure and appends an OutputLine to the OutputManager for each branch.""" branch_info = self.__branches_info[branch] if branch_info: branch_hash = branch_info.hash else: try: branch_hash = hash_one(branch, short=True) except subprocess2.CalledProcessError: branch_hash = None line = OutputLine() # The branch name with appropriate indentation. suffix = '' if branch == self.__current_branch or (self.__current_branch == 'HEAD' and branch == self.__current_hash): suffix = ' *' branch_string = branch if branch in self.__gone_branches: branch_string = '{%s:GONE}' % branch if not branch: branch_string = '{NO_UPSTREAM}' main_string = ' ' * depth + branch_string + suffix line.append(main_string, color=self.__color_for_branch(branch, branch_hash)) # The branch hash. if self.verbosity >= 2: line.append(branch_hash or '', separator=' ', color=Fore.RED) # The branch tracking status. if self.verbosity >= 1: ahead_string = '' behind_string = '' front_separator = '' center_separator = '' back_separator = '' if branch_info and not self.__is_invalid_parent( branch_info.upstream): ahead = branch_info.ahead behind = branch_info.behind if ahead: ahead_string = 'ahead %d' % ahead if behind: behind_string = 'behind %d' % behind if ahead or behind: front_separator = '[' back_separator = ']' if ahead and behind: center_separator = '|' line.append(front_separator, separator=' ') line.append(ahead_string, separator=' ', color=Fore.MAGENTA) line.append(center_separator, separator=' ') line.append(behind_string, separator=' ', color=Fore.MAGENTA) line.append(back_separator) # The Rietveld issue associated with the branch. if self.verbosity >= 2: import git_cl # avoid heavy import cost unless we need it none_text = '' if self.__is_invalid_parent(branch) else 'None' url = git_cl.Changelist( branchref=branch).GetIssueURL() if branch_hash else None line.append(url or none_text, color=Fore.BLUE if url else Fore.WHITE) self.output.append(line) for child in sorted(self.__parent_map.pop(branch, ())): self.__append_branch(child, depth=depth + 1)
def __append_branch(self, branch, depth=0): """Recurses through the tree structure and appends an OutputLine to the OutputManager for each branch.""" branch_info = self.__branches_info[branch] if branch_info: branch_hash = branch_info.hash else: try: branch_hash = hash_one(branch, short=True) except subprocess2.CalledProcessError: branch_hash = None line = OutputLine() # The branch name with appropriate indentation. suffix = '' if branch == self.__current_branch or ( self.__current_branch == 'HEAD' and branch == self.__current_hash): suffix = ' *' branch_string = branch if branch in self.__gone_branches: branch_string = '{%s:GONE}' % branch if not branch: branch_string = '{NO_UPSTREAM}' main_string = ' ' * depth + branch_string + suffix line.append( main_string, color=self.__color_for_branch(branch, branch_hash)) # The branch hash. if self.verbosity >= 2: line.append(branch_hash or '', separator=' ', color=Fore.RED) # The branch tracking status. if self.verbosity >= 1: ahead_string = '' behind_string = '' front_separator = '' center_separator = '' back_separator = '' if branch_info and not self.__is_invalid_parent(branch_info.upstream): ahead = branch_info.ahead behind = branch_info.behind if ahead: ahead_string = 'ahead %d' % ahead if behind: behind_string = 'behind %d' % behind if ahead or behind: front_separator = '[' back_separator = ']' if ahead and behind: center_separator = '|' line.append(front_separator, separator=' ') line.append(ahead_string, separator=' ', color=Fore.MAGENTA) line.append(center_separator, separator=' ') line.append(behind_string, separator=' ', color=Fore.MAGENTA) line.append(back_separator) # The Rietveld issue associated with the branch. if self.verbosity >= 2: none_text = '' if self.__is_invalid_parent(branch) else 'None' (url, color) = self.__status_info[branch] line.append(url or none_text, color=color) # The subject of the most recent commit on the branch. if self.show_subject: line.append(run('log', '-n1', '--format=%s', branch)) self.output.append(line) for child in sorted(self.__parent_map.pop(branch, ())): self.__append_branch(child, depth=depth + 1)
def _upfn(b): parent = upstream(b) if parent: return hash_one(parent)
def hyper_blame(ignored, filename, revision='HEAD', out=sys.stdout, err=sys.stderr): # Map from commit to parsed blame from that commit. blame_from = {} def cache_blame_from(filename, commithash): try: return blame_from[commithash] except KeyError: parsed = get_parsed_blame(filename, commithash) blame_from[commithash] = parsed return parsed try: parsed = cache_blame_from(filename, git_common.hash_one(revision)) except subprocess2.CalledProcessError as e: err.write(e.stderr) return e.returncode new_parsed = [] # We don't show filenames in blame output unless we have to. show_filenames = False for line in parsed: # If a line references an ignored commit, blame that commit's parent # repeatedly until we find a non-ignored commit. while line.commit.commithash in ignored: if line.commit.previous is None: # You can't ignore the commit that added this file. break previouscommit, previousfilename = line.commit.previous.split(' ', 1) parent_blame = cache_blame_from(previousfilename, previouscommit) if len(parent_blame) == 0: # The previous version of this file was empty, therefore, you can't # ignore this commit. break # line.lineno_then is the line number in question at line.commit. We need # to translate that line number so that it refers to the position of the # same line on previouscommit. lineno_previous = approx_lineno_across_revs( line.commit.filename, previousfilename, line.commit.commithash, previouscommit, line.lineno_then) logging.debug('ignore commit %s on line p%d/t%d/n%d', line.commit.commithash, lineno_previous, line.lineno_then, line.lineno_now) # Get the line at lineno_previous in the parent commit. assert 1 <= lineno_previous <= len(parent_blame) newline = parent_blame[lineno_previous - 1] # Replace the commit and lineno_then, but not the lineno_now or context. logging.debug(' replacing with %r', newline) line = BlameLine(newline.commit, line.context, lineno_previous, line.lineno_now, True) # If any line has a different filename to the file's current name, turn on # filename display for the entire blame output. if line.commit.filename != filename: show_filenames = True new_parsed.append(line) pretty_print(new_parsed, show_filenames=show_filenames, out=out) return 0
def hyper_blame(outbuf, ignored, filename, revision): # Map from commit to parsed blame from that commit. blame_from = {} filename = os.path.normpath(filename) def cache_blame_from(filename, commithash): try: return blame_from[commithash] except KeyError: parsed = get_parsed_blame(filename, commithash) blame_from[commithash] = parsed return parsed try: parsed = cache_blame_from(filename, git_common.hash_one(revision)) except subprocess2.CalledProcessError as e: sys.stderr.write(e.stderr.decode()) return e.returncode new_parsed = [] # We don't show filenames in blame output unless we have to. show_filenames = False for line in parsed: # If a line references an ignored commit, blame that commit's parent # repeatedly until we find a non-ignored commit. while line.commit.commithash in ignored: if line.commit.previous is None: # You can't ignore the commit that added this file. break previouscommit, previousfilename = line.commit.previous.split( ' ', 1) parent_blame = cache_blame_from(previousfilename, previouscommit) if len(parent_blame) == 0: # The previous version of this file was empty, therefore, you can't # ignore this commit. break # line.lineno_then is the line number in question at line.commit. We need # to translate that line number so that it refers to the position of the # same line on previouscommit. lineno_previous = approx_lineno_across_revs( line.commit.filename, previousfilename, line.commit.commithash, previouscommit, line.lineno_then) logging.debug('ignore commit %s on line p%d/t%d/n%d', line.commit.commithash, lineno_previous, line.lineno_then, line.lineno_now) # Get the line at lineno_previous in the parent commit. assert 1 <= lineno_previous <= len(parent_blame) newline = parent_blame[lineno_previous - 1] # Replace the commit and lineno_then, but not the lineno_now or context. line = BlameLine(newline.commit, line.context, newline.lineno_then, line.lineno_now, True) logging.debug(' replacing with %r', line) # If any line has a different filename to the file's current name, turn on # filename display for the entire blame output. # Use normpath to make variable consistent across platforms. if os.path.normpath(line.commit.filename) != filename: show_filenames = True new_parsed.append(line) pretty_print(outbuf, new_parsed, show_filenames=show_filenames) return 0
def rebase_branch(branch, parent, start_hash): logging.debug('considering %s(%s) -> %s(%s) : %s', branch, git.hash_one(branch), parent, git.hash_one(parent), start_hash) # If parent has FROZEN commits, don't base branch on top of them. Instead, # base branch on top of whatever commit is before them. back_ups = 0 orig_parent = parent while git.run('log', '-n1', '--format=%s', parent, '--').startswith(git.FREEZE): back_ups += 1 parent = git.run('rev-parse', parent + '~') if back_ups: logging.debug('Backed parent up by %d from %s to %s', back_ups, orig_parent, parent) if git.hash_one(parent) != start_hash: # Try a plain rebase first print 'Rebasing:', branch rebase_ret = git.rebase(parent, start_hash, branch, abort=True) if not rebase_ret.success: # TODO(iannucci): Find collapsible branches in a smarter way? print "Failed! Attempting to squash", branch, "...", squash_branch = branch + "_squash_attempt" git.run('checkout', '-b', squash_branch) git.squash_current_branch(merge_base=start_hash) # Try to rebase the branch_squash_attempt branch to see if it's empty. squash_ret = git.rebase(parent, start_hash, squash_branch, abort=True) empty_rebase = git.hash_one(squash_branch) == git.hash_one(parent) git.run('checkout', branch) git.run('branch', '-D', squash_branch) if squash_ret.success and empty_rebase: print 'Success!' git.squash_current_branch(merge_base=start_hash) git.rebase(parent, start_hash, branch) else: print "Failed!" print # rebase and leave in mid-rebase state. # This second rebase attempt should always fail in the same # way that the first one does. If it magically succeeds then # something very strange has happened. second_rebase_ret = git.rebase(parent, start_hash, branch) if second_rebase_ret.success: # pragma: no cover print "Second rebase succeeded unexpectedly!" print "Please see: http://crbug.com/425696" print "First rebased failed with:" print rebase_ret.stderr else: print "Here's what git-rebase (squashed) had to say:" print print squash_ret.stdout print squash_ret.stderr print textwrap.dedent("""\ Squashing failed. You probably have a real merge conflict. Your working copy is in mid-rebase. Either: * completely resolve like a normal git-rebase; OR * abort the rebase and mark this branch as dormant: git config branch.%s.dormant true And then run `git rebase-update` again to resume. """ % branch) return False else: print '%s up-to-date' % branch git.remove_merge_base(branch) git.get_or_create_merge_base(branch) return True
def __append_branch(self, branch, depth=0): """Recurses through the tree structure and appends an OutputLine to the OutputManager for each branch.""" branch_info = self.__branches_info[branch] if branch_info: branch_hash = branch_info.hash else: branch_hash = hash_one(branch, short=True) line = OutputLine() # The branch name with appropriate indentation. suffix = '' if branch == self.__current_branch or ( self.__current_branch == 'HEAD' and branch == self.__current_hash): suffix = ' *' branch_string = branch if branch in self.__gone_branches: branch_string = '{%s:GONE}' % branch if not branch: branch_string = '{NO_UPSTREAM}' main_string = ' ' * depth + branch_string + suffix line.append( main_string, color=self.__color_for_branch(branch, branch_hash)) # The branch hash. if self.verbosity >= 2: line.append(branch_hash or '', separator=' ', color=Fore.RED) # The branch tracking status. if self.verbosity >= 1: ahead_string = '' behind_string = '' front_separator = '' center_separator = '' back_separator = '' if branch_info and not self.__is_invalid_parent(branch_info.upstream): ahead = branch_info.ahead behind = branch_info.behind if ahead: ahead_string = 'ahead %d' % ahead if behind: behind_string = 'behind %d' % behind if ahead or behind: front_separator = '[' back_separator = ']' if ahead and behind: center_separator = '|' line.append(front_separator, separator=' ') line.append(ahead_string, separator=' ', color=Fore.MAGENTA) line.append(center_separator, separator=' ') line.append(behind_string, separator=' ', color=Fore.MAGENTA) line.append(back_separator) # The Rietveld issue associated with the branch. if self.verbosity >= 2: import git_cl # avoid heavy import cost unless we need it none_text = '' if self.__is_invalid_parent(branch) else 'None' url = git_cl.Changelist(branchref=branch).GetIssueURL() line.append(url or none_text, color=Fore.BLUE if url else Fore.WHITE) self.output.append(line) for child in sorted(self.__parent_map.pop(branch, ())): self.__append_branch(child, depth=depth + 1)