def arg_type_git_remote_or_local(string): # See: https://stackoverflow.com/questions/9610131/how-to-check-the-validity-of-a-remote-git-repository-url try: launch(["git", "ls-remote", string], epfx="Cannot handle '%s' as a Git repository" % string) except: raise ArgumentTypeError("\n" + format_exc()) return string
def arg_type_git_ref_name_internal(ref, string): full_name = "refs/%ss/%s" % (ref, string) try: launch(["git", "check-ref-format", full_name], epfx="Incorrect %s name '%s' (%s)" % (ref, string, full_name)) except: raise ArgumentTypeError("\n" + format_exc()) return full_name
def git2(self, *cmd_args): cwd = getcwd() if cwd != self.path: chdir(self.path) self._stdout, self._stderr = launch((self._ctx.git_command, ) + cmd_args)
def __init__(self, git_command="git", cache_path=None, from_cache=False, **kw): super(GitContext, self).__init__(git_command=git_command, cache_path=cache_path, from_cache=from_cache, **kw) self._sha2commit = {} self._origin2cloned = {} # get version of git _stdout, _stderr = launch([self.git_command, "--version"], epfx="Cannot get version of git") _, _, version = _stdout.split(b" ")[:3] self._git_version = tuple(int(v) for v in version.split(b".")[:3]) # walk cache_path and fill _cahce self._cache = cache = {} # refer the cache by absolute path cwd = getcwd() cache_path = self.cache_path if not cache_path.startswith(cwd): cache_path = join(cwd, cache_path) self.cache_path = cache_path backup_dir = join(cache_path, "backup") if cache_path: for root, _, files in walk(cache_path): if root.startswith(backup_dir): continue for f in files: # First 40 characters of the a file name must be the SHA1 # of the corresponding commit. if not cache_file_re.match(f): continue key = b(f[:40].lower()) cached_file = join(root, f) if key in cache: print("Multiple entries for %s found in the " "cache:\n '%s'\n '%s'" % (u(key), cached_file, cache[key])) continue cache[key] = cached_file print("cache = " + str(self._cache).replace(",", ",\n ").replace( "{", "{\n ").replace("}", "\n}"))
def run(self): env = common.load_env(self.log) if env['game_dir'] is None: return ca_bundle_path = common.locate_ca_bundle(env['script_dir']) if ca_bundle_path is None: common.get_node( { 'src': 'https://curl.haxx.se/ca/cacert.pem', 'dest': 'cacert.pem' }, False, False, env['script_dir'], self.log) self.log.log_ts('CA Bundle updated', info=True) addons = common.list_addons(env, self.log) update_context = {'launch': True, 'error': False} common.update_addons(env, addons, self.log, False, update_context) if update_context['error']: pass elif update_context['launch']: common.launch(env['game_dir'], env['game_args'])
def launch(self, *cmd_args): cwd = getcwd() if cwd != self.path: chdir(self.path) out, err = launch(cmd_args) if out: self._out(out) if err: self._err(err)
def arg_type_git(string): try: _stdout, _ = launch([string, "--version"], epfx="Launch of '%s --version' failed" % string) except: raise ArgumentTypeError("\n" + format_exc()) try: words = _stdout.split(b" ") if len(words) < 3: raise ValueError("Too few words %d, expected at least 3." % len(words)) except: raise ArgumentTypeError( "Cannot tokenize output\n%s\nunderlying error:\n%s" % (_stdout, format_exc())) if [b"git", b"version"] != words[0:2]: raise ArgumentTypeError( "Unexpected version string '%s', expected 'git version ...'" % ( b" ".join(words[0:3]) # add third word )) return string
def main(): print("Git Interactive Cloner") init_cwd = getcwd() ap = ArgumentParser() ap.add_argument("source", type=arg_type_git_repository, nargs="?") ap.add_argument("-d", "--destination", type=arg_type_new_directory) ap.add_argument("-r", "--result-state", type=arg_type_output_file) ap.add_argument("-m", "--main-stream", type=arg_type_SHA1_lower, metavar="SHA1", help="""\ Set main stream by SHA1 of one of main stream commits. Only main stream commits will be cloned. Other commits will be taken as is. A commit belongs to main stream if it is a descendant of the given commit or both have at least one common ancestor. Commonly, SHA1 corresponds to main stream initial commit.""") ap.add_argument("-b", "--break", type=arg_type_SHA1_lower, action='append', dest="breaks", metavar="SHA1", help="""\ Specify break points. A break point is set on the commit identified by SHA1. \ The process will be interrupted after the commit allowing a user to change it. \ The tool will recover original committer name, e-mail and date during the next \ launch.""") ap.add_argument("-s", "--skip", type=arg_type_SHA1_lower, action='append', dest="skips", metavar="SHA1", help="""\ Specify a commit to skip. Use multiple options to skip several commits. If a \ commit at break point is skipped then interruption will be made after \ previous non-skipped commit in the branch except for no commits are copied yet \ since either trunk or root.""") ap.add_argument("-H", "--head", type=arg_type_git_head_name, action='append', dest="refs", metavar="name_of_head", help="""\ Copy commits those are ancestors of selected heads only (including the heads).""") ap.add_argument("-t", "--tag", type=arg_type_git_tag_name, action='append', dest="refs", metavar="name_of_tag", help="""\ Copy commits those are ancestors of selected tags only (including the tag).""") ap.add_argument("-i", "--insert-before", type=composite_type(arg_type_SHA1_lower, arg_type_input_file), action='append', nargs=2, dest="insertions", metavar=("SHA1", "COMMIT"), help="""\ Insert COMMIT before the commit with SHA1. COMMIT must be defined by a path of the patch file in 'git am' compatible format.""") ap.add_argument("-g", "--git", type=arg_type_git, default="git", metavar="path/to/alternative/git", help="""Use explicit git executable.""") ap.add_argument("-l", "--log", type=arg_type_output_file, default=LOG_STANDARD, metavar="path/to/log.csv", help="Log git`s standard output and errors to that file.") ap.add_argument( "-c", "--cache", type=arg_type_directory, metavar="path/to/cache", dest="cache_path", help="""Resolve conflicts or edit break point commits using patches from the cache. A patch file name must start with SHA1 of corresponding original commit.""" # TODO: User modifications will be also preserved in the cache. ) ap.add_argument( "--from-cache", action="store_true", help="""If a patch is found in the cache then the process will not be interrupted on either a conflicts or a break point. All changes is taken from that patch.""") args = ap.parse_args() ctx = None if isfile(STATE_FILE_NAME): try: ctx = load_context(STATE_FILE_NAME) except: print("Incorrect state file") print_exc(file=sys.stdout) cloned_source = None if ctx is None: git_cmd = args.git source = args.source if source is None: print("No source repository path was given.") ap.print_help(sys.stdout) return try: local = arg_type_git_local(source) except ArgumentTypeError: remote = source # Source points to a remote repository. It must be cloned first # because git.Repo cannot work with a remote repository. cloned_source = join(init_cwd, ".gic-cloned-source") try: cloned_source = arg_type_new_directory(cloned_source) except ArgumentTypeError: raise RuntimeError( "Cannot clone source repository into local " "temporal directory '%s', underlying error:\n%s" % (cloned_source, format_exc())) print("Cloning source repository into local temporal directory " "'%s'" % cloned_source) # delete existing copy if isdir(cloned_source): rmtree(cloned_source) try: launch([git_cmd, "clone", remote, cloned_source], epfx="Cloning has failed") except: sys.stderr.write("\n" + format_exc()) rmtree(cloned_source) exit(1) # create all branches in temporal copy tmp_repo = Repo(cloned_source) chdir(cloned_source) for ref in list(tmp_repo.references): if not ref.path.startswith("refs/remotes/origin/"): continue # cut out prefix "origin/" branch = ref.name[7:] if branch == "HEAD" or branch == "master": continue try: launch( [git_cmd, "branch", branch, ref.name], epfx="Cannot create tracking branch '%s' in temporal" " copy of origin repository" % branch) except: chdir(init_cwd) rmtree(cloned_source) sys.stderr.write("\n" + format_exc()) exit(1) chdir(init_cwd) srcRepoPath = cloned_source else: # Source points to a local repository. srcRepoPath = local log = args.log # overwrite log if log is not LOG_STANDARD and isfile(log): unlink(log) ctx = GitContext(src_repo_path=srcRepoPath, git_command=git_cmd, cache_path=args.cache_path, from_cache=args.from_cache, log=log) switch_context(ctx) else: srcRepoPath = ctx.src_repo_path print("Building graph of repository: " + srcRepoPath) repo = Repo(srcRepoPath) sha2commit = ctx._sha2commit GICCommitDesc.build_git_graph(repo, sha2commit, skip_remotes=True, skip_stashes=True, refs=args.refs) print("Total commits: %d" % len(sha2commit)) if ctx.current_action < 0: destination = args.destination if destination is None: print("No destination specified. Dry run.") return dstRepoPath = destination ms = args.main_stream if ms: ms_bits = sha2commit[ms].roots else: ms_bits = 0 print("The repository will be cloned to: " + dstRepoPath) # Planing plan(repo, sha2commit, dstRepoPath, breaks=args.breaks, skips=args.skips, main_stream_bits=ms_bits, insertions=args.insertions) # remove temporal clone of the source repository if cloned_source: RemoveDirectory(path=cloned_source) else: print("The context was loaded. Continuing...") ctx.restore_cloned() ctx.do() # save results if getcwd() != init_cwd: chdir(init_cwd) if ctx.finished: if isfile(STATE_FILE_NAME): unlink(STATE_FILE_NAME) else: pythonize(ctx, STATE_FILE_NAME + ".tmp") if isfile(STATE_FILE_NAME): unlink(STATE_FILE_NAME) rename(STATE_FILE_NAME + ".tmp", STATE_FILE_NAME) rs = args.result_state if rs: pythonize(ctx, rs)
def __call__(self): ctx = self._ctx cache = ctx._cache sha = self.commit_sha if sha not in cache: return patch_file_name = cache[sha] print("Applying changes from " + patch_file_name) # analyze the patch and prepare working directory to patching p = open(patch_file_name, "rb") liter = iter(list(p.readlines())) p.close() msg = b"" for l in liter: minfo = re_commit_message.match(l) if minfo: msg += minfo.group(3) break else: raise ValueError("Incorrect patch file: no commit message.") msg_lines = [] for l in liter: if l.startswith(b"diff --git ") or l.startswith(b"---"): msg += b"".join(msg_lines[:-1]) break msg_lines.append(l) else: raise ValueError( "Incorrect patch file: commit message terminator is not found." ) changed_files = set() created_files = set() deleted_files = set() for l in liter: if l.startswith(b"--- a/"): file_name = l[6:].strip(b"\n\r") l = next(liter) if l.startswith(b"--- /dev/null"): deleted_files.add(file_name) else: changed_files.add(file_name) elif l.startswith(b"--- /dev/null"): l = next(liter) created_files.add(l[6:].strip(b"\n\r")) # for n in ["changed_files", "created_files", "deleted_files"]: # print(n + " = " + str(locals()[n]) # .replace("set([", "set([\n ") # .replace("'])", "'\n])") # .replace("', '", "',\n '") # ) s2c = ctx._sha2commit c = s2c[sha] if changed_files or deleted_files: p = c.parents[0] p_cloned_sha = p.cloned_sha for cf in chain(changed_files, deleted_files): self.git("checkout", p_cloned_sha, cf) for f in created_files: if isfile(f): self.launch("rm", f) # actual patching try: out, err = launch(["patch", "-p", "1", "-i", patch_file_name], epfx="Failed to apply changes from '%s'" % patch_file_name) except LaunchFailed as e: out, err = e._stdout, e._stderr Interrupt(reason=str(e)) if out: self._out(out) if err: self._err(err) # apply commit message merge_msg_path = join(self.path, ".git", "MERGE_MSG") if isfile(merge_msg_path): # a merge is in progress msg_f = open(merge_msg_path, "wb") msg_f.write(msg) msg_f.close() else: self.git("commit", "--only", "--amend", "-m", msg) self._changed_files = changed_files self._deleted_files = deleted_files self._created_files = created_files