def pull_repo(repo_path: Path): """Update a repository at repo_path by pulling from the remote named origin.""" repo = Repository(repo_path) remote = repo.remotes['origin'] remote.fetch() master_id = repo.lookup_reference('refs/remotes/origin/master').target merge_result, _ = repo.merge_analysis(master_id) if merge_result & GIT_MERGE_ANALYSIS_UP_TO_DATE: return if merge_result & GIT_MERGE_ANALYSIS_FASTFORWARD: repo.checkout_tree(repo.get(master_id)) master_ref = repo.lookup_reference('refs/heads/master') master_ref.set_target(master_id) repo.head.set_target(master_id) elif merge_result & GIT_MERGE_ANALYSIS_NORMAL: repo.merge(master_id) assert repo.index.conflicts is None, \ 'Merge conflicts, please manually fix' tree = repo.index.write_tree() repo.create_commit('refs/heads/master', SIGNATURE, SIGNATURE, '[build-server]: Merge', tree, [repo.head.target, master_id]) repo.state_cleanup()
def merge_squash( repo: pygit2.Repository, ours_branch: pygit2.Branch, theirs_branch: pygit2.Branch, message: str, ) -> None: """ Performs a merge of the `theirs_branch` into `ours_branch` sqaushing the commits """ merge_state, _ = repo.merge_analysis(theirs_branch.target, ours_branch.name) if merge_state & pygit2.GIT_MERGE_ANALYSIS_UP_TO_DATE: return if not (merge_state & pygit2.GIT_MERGE_ANALYSIS_FASTFORWARD): raise ValueError(theirs_branch) index: pygit2.Index = repo.merge_trees( ancestor=ours_branch, ours=ours_branch, theirs=theirs_branch ) tree = index.write_tree(repo=repo) repo.create_commit( ours_branch.name, repo.default_signature, repo.default_signature, message, tree, [ours_branch.target], )
def sync(repo: pygit2.Repository, branch_name: str) -> None: """ Tries to update the `branch_name` branch of the `repo` repo to the latest upstream branch state. If the branch is up to date, does nothing. If the branch can be fast-forwarded, resets to the upstream. Otherwise, fails with an error. """ branch = repo.branches.local[branch_name] if not branch.is_head(): raise ValueError(branch) try: remote = repo.remotes['origin'] except KeyError: return remote.fetch(callbacks=RemoteCallbacks()) upstream_branch = branch.upstream if not upstream_branch: return merge_state, _ = repo.merge_analysis(upstream_branch.target, branch.name) if merge_state & pygit2.GIT_MERGE_ANALYSIS_UP_TO_DATE: return if not (merge_state & pygit2.GIT_MERGE_ANALYSIS_FASTFORWARD): raise ValueError(branch) repo.reset(upstream_branch.target, pygit2.GIT_RESET_HARD) repo.checkout(refname=branch)
def finish(repo: pygit2.Repository, branch_name: str, message: str) -> None: master = repo.branches.local['master'] branch = repo.branches.local[branch_name] if not branch.is_head(): raise ValueError(branch) merge_state, _ = repo.merge_analysis(branch.target, master.name) if merge_state & pygit2.GIT_MERGE_ANALYSIS_UP_TO_DATE: repo.checkout(refname=master) return if not (merge_state & pygit2.GIT_MERGE_ANALYSIS_FASTFORWARD): raise ValueError(branch) index: pygit2.Index = repo.merge_trees(ancestor=master, ours=master, theirs=branch) tree = index.write_tree(repo=repo) repo.create_commit( master.name, repo.default_signature, repo.default_signature, message, tree, [master.target], ) repo.checkout(refname=master) branch.delete()
def pull(repo: pygit2.Repository, remote_name: str = 'origin'): """ Pull :param repo: the repository to pull :param remote_name: name of the remote :return: """ for remote in repo.remotes: if remote.name == remote_name: remote.fetch() remote_master_id = repo.lookup_reference('refs/remotes/origin/master').target merge_result, _ = repo.merge_analysis(remote_master_id) # Up to date, do nothing if merge_result & pygit2.GIT_MERGE_ANALYSIS_UP_TO_DATE: return # We can just fastforward elif merge_result & pygit2.GIT_MERGE_ANALYSIS_FASTFORWARD: repo.checkout_tree(repo.get(remote_master_id)) master_ref = repo.lookup_reference('refs/heads/master') master_ref.set_target(remote_master_id) repo.head.set_target(remote_master_id) elif merge_result & pygit2.GIT_MERGE_ANALYSIS_NORMAL: repo.merge(remote_master_id) print(repo.index.conflicts) assert repo.index.conflicts is None, 'Conflicts, ahhhh!' user = repo.default_signature tree = repo.index.write_tree() repo.create_commit('HEAD', user, user, 'Merge!', tree, [repo.head.target, remote_master_id]) repo.state_cleanup() else: raise AssertionError('Unknown merge analysis result')
class GitRepo: """A class that manages a git repository. This class enables versiong via git for a repository. You can stage and commit files and checkout different commits of the repository. """ path = '' pathspec = [] repo = None callback = None author_name = 'QuitStore' author_email = '*****@*****.**' gcProcess = None def __init__(self, path, origin=None, gc=False): """Initialize a new repository from an existing directory. Args: path: A string containing the path to the repository. origin: The remote URL where to clone and fetch from and push to """ logger = logging.getLogger('quit.core.GitRepo') logger.debug('GitRepo, init, Create an instance of GitStore') self.path = path self.gc = gc if not exists(path): try: makedirs(path) except OSError as e: raise Exception('Can\'t create path in filesystem:', path, e) try: self.repo = Repository(path) except KeyError: pass except AttributeError: pass if origin: self.callback = QuitRemoteCallbacks() if self.repo: if self.repo.is_bare: raise QuitGitRepoError('Bare repositories not supported, yet') if origin: # set remote self.addRemote('origin', origin) else: if origin: # clone self.repo = self.cloneRepository(origin, path, self.callback) else: self.repo = init_repository(path=path, bare=False) def cloneRepository(self, origin, path, callback): try: repo = clone_repository(url=origin, path=path, bare=False, callbacks=callback) return repo except Exception as e: raise QuitGitRepoError( "Could not clone from: {} origin. {}".format(origin, e)) def addall(self): """Add all (newly created|changed) files to index.""" self.repo.index.read() self.repo.index.add_all(self.pathspec) self.repo.index.write() def addfile(self, filename): """Add a file to the index. Args: filename: A string containing the path to the file. """ index = self.repo.index index.read() try: index.add(filename) index.write() except Exception as e: logger.info( "GitRepo, addfile, Could not add file {}.".format(filename)) logger.debug(e) def addRemote(self, name, url): """Add a remote. Args: name: A string containing the name of the remote. url: A string containing the url to the remote. """ try: self.repo.remotes.create(name, url) logger.info("Successfully added remote: {} - {}".format(name, url)) except Exception as e: logger.info("Could not add remote: {} - {}".format(name, url)) logger.debug(e) try: self.repo.remotes.set_push_url(name, url) self.repo.remotes.set_url(name, url) except Exception as e: logger.info("Could not set push/fetch urls: {} - {}".format( name, url)) logger.debug(e) def checkout(self, commitid): """Checkout a commit by a commit id. Args: commitid: A string cotaining a commitid. """ try: commit = self.repo.revparse_single(commitid) self.repo.set_head(commit.oid) self.repo.reset(commit.oid, GIT_RESET_HARD) logger.info("Checked out commit: {}".format(commitid)) except Exception as e: logger.info("Could not check out commit: {}".format(commitid)) logger.debug(e) def commit(self, message=None): """Commit staged files. Args: message: A string for the commit message. Raises: Exception: If no files in staging area. """ if self.isstagingareaclean(): # nothing to commit return index = self.repo.index index.read() tree = index.write_tree() try: author = Signature(self.author_name, self.author_email) comitter = Signature(self.author_name, self.author_email) if len(self.repo.listall_reference_objects()) == 0: # Initial Commit if message is None: message = 'Initial Commit from QuitStore' self.repo.create_commit('HEAD', author, comitter, message, tree, []) else: if message is None: message = 'New Commit from QuitStore' self.repo.create_commit('HEAD', author, comitter, message, tree, [self.repo.head.get_object().hex]) logger.info('Updates commited') except Exception as e: logger.info('Nothing to commit') logger.debug(e) if self.gc: self.garbagecollection() def commitexists(self, commitid): """Check if a commit id is part of the repository history. Args: commitid: String of a Git commit id. Returns: True, if commitid is part of commit log False, else. """ if commitid in self.getids(): return True else: return False def garbagecollection(self): """Start garbage collection. Args: commitid: A string cotaining a commitid. """ try: # Check if the garbage collection process is still running if self.gcProcess is None or self.gcProcess.poll() is not None: # Start garbage collection with "--auto" option, # which imidietly terminates, if it is not necessary self.gcProcess = Popen(["git", "gc", "--auto", "--quiet"], cwd=self.path) logger.debug('Spawn garbage collection') except Exception as e: logger.debug('Git garbage collection failed to spawn') logger.debug(e) def getpath(self): """Return the path of the git repository. Returns: A string containing the path to the directory of git repo """ return self.path def getcommits(self): """Return meta data about exitsting commits. Returns: A list containing dictionaries with commit meta data """ commits = [] if len(self.repo.listall_reference_objects()) > 0: for commit in self.repo.walk(self.repo.head.target, GIT_SORT_REVERSE): commits.append({ 'id': str(commit.oid), 'message': str(commit.message), 'commit_date': datetime.fromtimestamp( commit.commit_time).strftime('%Y-%m-%dT%H:%M:%SZ'), 'author_name': commit.author.name, 'author_email': commit.author.email, 'parents': [c.hex for c in commit.parents], }) return commits def getids(self): """Return meta data about exitsting commits. Returns: A list containing dictionaries with commit meta data """ ids = [] if len(self.repo.listall_reference_objects()) > 0: for commit in self.repo.walk(self.repo.head.target, GIT_SORT_REVERSE): ids.append(str(commit.oid)) return ids def isgarbagecollectionon(self): """Return if gc is activated or not. Returns: True, if activated False, if not """ return self.gc def isstagingareaclean(self): """Check if staging area is clean. Returns: True, if staginarea is clean False, else. """ status = self.repo.status() for filepath, flags in status.items(): if flags != GIT_STATUS_CURRENT: return False return True def pull(self, remote='origin', branch='master'): """Pull if possible. Return: True: If successful. False: If merge not possible or no updates from remote. """ try: self.repo.remotes[remote].fetch() except Exception as e: logger.info("Can not pull: Remote {} not found.".format(remote)) logger.debug(e) ref = 'refs/remotes/' + remote + '/' + branch remoteid = self.repo.lookup_reference(ref).target analysis, _ = self.repo.merge_analysis(remoteid) if analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE: # Already up-to-date pass elif analysis & GIT_MERGE_ANALYSIS_FASTFORWARD: # fastforward self.repo.checkout_tree(self.repo.get(remoteid)) master_ref = self.repo.lookup_reference('refs/heads/master') master_ref.set_target(remoteid) self.repo.head.set_target(remoteid) elif analysis & GIT_MERGE_ANALYSIS_NORMAL: self.repo.merge(remoteid) tree = self.repo.index.write_tree() msg = 'Merge from ' + remote + ' ' + branch author = Signature(self.author_name, self.author_email) comitter = Signature(self.author_name, self.author_email) self.repo.create_commit('HEAD', author, comitter, msg, tree, [self.repo.head.target, remoteid]) self.repo.state_cleanup() else: logger.debug('Can not pull. Unknown merge analysis result') def push(self, remote='origin', branch='master'): """Push if possible. Return: True: If successful. False: If diverged or nothing to push. """ ref = ['refs/heads/' + branch] try: remo = self.repo.remotes[remote] except Exception as e: logger.info( "Can not push. Remote: {} does not exist.".format(remote)) logger.debug(e) return try: remo.push(ref, callbacks=self.callback) except Exception as e: logger.info("Can not push to {} with ref {}".format( remote, str(ref))) logger.debug(e) def getRemotes(self): remotes = {} try: for remote in self.repo.remotes: remotes[remote.name] = [remote.url, remote.push_url] except Exception as e: logger.info('No remotes found.') logger.debug(e) return {} return remotes