def write_tree(self): tools = Tools() with tools.cwd(self._git_folder()): tools.copy("index", "jolt-index") with tools.environ(GIT_INDEX_FILE=self._git_jolt_index()), tools.cwd( self.path): tree = tools.run( "git -c core.safecrlf=false add -A && git write-tree", output_on_error=True) return tree
class GitRepository(object): def __init__(self, url, path, relpath, refspecs=None): self.path = path self.relpath = relpath self.tools = Tools() self.url = url self.default_refspecs = [ '+refs/heads/*:refs/remotes/origin/*', '+refs/tags/*:refs/remotes/origin/*', ] self.refspecs = refspecs or [] self._tree_hash = {} self._original_head = True self._init_repo() def _init_repo(self): self.repository = pygit2.Repository(self.path) if os.path.exists( self._git_folder()) else None @utils.cached.instance def _git_folder(self): return fs.path.join(self.path, ".git") @utils.cached.instance def _git_index(self): return fs.path.join(self.path, ".git", "index") @utils.cached.instance def _git_jolt_index(self): return fs.path.join(self.path, ".git", "jolt-index") def is_cloned(self): return fs.path.exists(self._git_folder()) def is_indexed(self): return fs.path.exists(self._git_index()) def clone(self): log.info("Cloning into {0}", self.path) if fs.path.exists(self.path): with self.tools.cwd(self.path): self.tools.run( "git init && git remote add origin {} && git fetch", self.url, output_on_error=True) else: self.tools.run("git clone {0} {1}", self.url, self.path, output_on_error=True) raise_error_if(not fs.path.exists(self._git_folder()), "git: failed to clone repository '{0}'", self.relpath) self._init_repo() @utils.cached.instance def diff_unchecked(self): if not self.is_indexed(): return "" # Build the jolt index file self.write_tree() # Diff against the jolt index with self.tools.environ( GIT_INDEX_FILE=self._git_jolt_index()), self.tools.cwd( self.path): return self.tools.run("git diff --binary --no-ext-diff HEAD ./", output_on_error=True, output_rstrip=False) def diff(self): diff = self.diff_unchecked() dlim = config.getsize("git", "maxdiffsize", "1M") raise_error_if( len(diff) > dlim, "git patch for '{}' exceeds configured size limit of {} bytes - actual size {}" .format(self.path, dlim, len(diff))) return diff def patch(self, patch): if not patch: return with self.tools.cwd(self.path), self.tools.tmpdir("git") as t: patchfile = fs.path.join(t.path, "jolt.diff") with open(patchfile, "wb") as f: f.write(patch.encode()) log.info("Applying patch to {0}", self.path) self.tools.run("git apply --whitespace=nowarn {patchfile}", patchfile=patchfile) def head(self): if not self.is_cloned(): return None return str(self.repository.head.target) def is_head(self, rev): return self.is_indexed() and self.head() == rev def is_original_head(self): return self._original_head def is_valid_sha(self, rev): return re.match(r"[0-9a-f]{40}", rev) def rev_parse(self, rev): if self.is_valid_sha(rev): return rev with self.tools.cwd(self.path): try: commit = self.repository.revparse_single(rev) except KeyError: self.fetch() try: commit = self.repository.revparse_single(rev) except Exception: raise_error("invalid git reference: {}", rev) try: return str(commit.id) except Exception: return str(commit) @utils.cached.instance def write_tree(self): tools = Tools() with tools.cwd(self._git_folder()): tools.copy("index", "jolt-index") with tools.environ(GIT_INDEX_FILE=self._git_jolt_index()), tools.cwd( self.path): tree = tools.run( "git -c core.safecrlf=false add -A && git write-tree", output_on_error=True) return tree def tree_hash(self, sha=None, path="/"): # When sha is None, the caller want the tree hash of the repository's # current workspace state. If no checkout has been made, that would be the # tree that was written upon initialization of the repository as it # includes any uncommitted changes. If a checkout has been made since # the repo was initialized, make this an explicit request for the current # head - there can be no local changes. if sha is None: if self.is_original_head(): tree = self.repository.get(self.write_tree()) else: sha = self.head() path = fs.path.normpath(path) full_path = fs.path.join(self.path, path) if path != "/" else self.path # Lookup tree hash value in cache value = self._tree_hash.get((full_path, sha)) if value is not None: return value # Translate explicit sha to tree if sha is not None: commit = self.rev_parse(sha) obj = self.repository.get(commit) try: tree = obj.tree except AttributeError: tree = obj.get_object().tree # Traverse tree from root to requested path if path != "/": tree = tree[fs.as_posix(path)] # Update tree hash cache self._tree_hash[(full_path, sha)] = value = tree.id return value def clean(self): with self.tools.cwd(self.path): return self.tools.run("git clean -dfx", output_on_error=True) def reset(self): with self.tools.cwd(self.path): return self.tools.run("git reset --hard", output_on_error=True) def fetch(self): refspec = " ".join(self.default_refspecs + self.refspecs) with self.tools.cwd(self.path): log.info("Fetching {0} from {1}", refspec or 'commits', self.url) self.tools.run("git fetch {url} {refspec}", url=self.url, refspec=refspec or '', output_on_error=True) def checkout(self, rev): log.info("Checking out {0} in {1}", rev, self.path) with self.tools.cwd(self.path): try: return self.tools.run("git checkout -f {rev}", rev=rev, output=False) except Exception: self.fetch() return self.tools.run("git checkout -f {rev}", rev=rev, output_on_error=True) self._original_head = False