def test_field_has_changed(test_row, test_column, test_value): our_model = EditableGitModel(REPOSITORY_NAME) our_model.populate() # print "====================================== Before the write" # for row in xrange(our_model.row_count()): # print pretty_print_from_row(our_model, row) # print "=======================================================" index = Index(test_row, test_column) our_model.start_history_event() our_model.set_data(index, test_value) write_and_wait(our_model) new_model = GitModel(REPOSITORY_NAME) new_model.populate() new_model_value = new_model.data(index) # print "======================================= After the write" # for row in xrange(our_model.row_count()): # print pretty_print_from_row(new_model, row) # print "=======================================================" if test_column in (1, 2): assert new_model_value[0] == test_value[0] and \ new_model_value[1].tzname("") == test_value[1].tzname(""), \ "The %s field wasn't changed correctly" % \ AVAILABLE_CHOICES[test_column] else: assert new_model_value == test_value, \ "The %s field wasn't changed correctly" % \ AVAILABLE_CHOICES[test_column] for row in xrange(our_model.row_count()): for column in xrange(1, our_model.column_count()): if (row == test_row and column == test_column): continue index = Index(row, column) our_value = our_model.data(index) new_value = new_model.data(index) if column in (1, 2): our_value, tz = our_value # print our_value, tz.tzname(None) new_value, tz = new_value # print new_value, tz.tzname(None) assert our_value == new_value, \ "Something else has change: (%d, %d)\ncolumn:%s\n" % \ (row, column, AVAILABLE_CHOICES[column]) + \ "%s\n%s\n%s\n" % \ (AVAILABLE_CHOICES, pretty_print_from_row(our_model, row), pretty_print_from_row(new_model, row)) + \ "%s // %s" % (our_value, new_value)
def populate(self): """ Populates the model, by constructing a list of the commits of the current branch of the given repository. """ self.init_attributes() if not self.is_fake_model(): self.orig_model.populate() GitModel.populate(self)
class EditableGitModel(GitModel): """ This class represents the list of commits of the current branch of a given repository. This class is meant to be Qt-free so that it can be used in other ways than gitbuster. """ def __init__(self, directory=".", fake_branch_name="", from_commits=False): """ Initializes the model with the repository root directory. :param directory: Root directory of the git repository. """ if fake_branch_name: # This is an empy gitModel that will be filled with data from # another model self.orig_model = None else: self.orig_model = GitModel(directory=directory) self.init_attributes() GitModel.__init__(self, directory=directory, fake_branch_name=fake_branch_name, from_commits=from_commits) def init_attributes(self): """ These attributes should be resetted when populating the model. """ self._modifications = {} self._deleted_commits = [] self._merge = False self._history = [] self._last_history_event = -1 self._conflicting_commit = None self._unmerged_files = None self._solutions = {} self._new_branch_name = "" self._git_process = None self._start_write_cache = {} self._children_cache = {} def populate(self): """ Populates the model, by constructing a list of the commits of the current branch of the given repository. """ self.init_attributes() if not self.is_fake_model(): self.orig_model.populate() GitModel.populate(self) def set_current_branch(self, branch, force=False): """ Sets the model's current branch. :param branch: The desired branch to modelize. """ if self._changed_branch_once and not force: raise GfbiException("You shouldn't change the branch twice.") if self.is_fake_model(): # This is the moment after we wrote the model, the model is getting # real (not fake). self.orig_model = GitModel(directory=self._directory) self.orig_model.set_current_branch(branch, force=force) self._modifications = {} GitModel.set_current_branch(self, branch, force=force) def get_modifications(self): """ Returns the modified values dictionnary. """ return self._modifications def get_modified_count(self): """ Returns the number of modified commits. """ modified = set( [commit for commit in self._commits if self.commit_is_modified(commit) and not self.is_deleted(commit)]) return len(modified) def get_deleted_count(self): """ Returns the number of deleted commits. """ deleted = set( [commit for commit in self._commits if self.is_deleted(commit)]) return len(deleted) def get_orig_model(self): """ Returns an unmodified GitModel. """ return self.orig_model def data(self, index): """ This method uses the index row to select the commit and the index column to select the field to return. :param index: The index of the wanted information. :return: Depending on the index column, one of the commit fields. """ if self.is_modified(index): return self.modified_data(index) else: return self.orig_data(index) def c_data(self, commit, field): """ This is a convenient method to access data using the commit and the column. """ row = self.row_of(commit) col = self.get_column(field) return self.data(Index(row, col)) def modified_data(self, index): commit = self._commits[index.row()] column = index.column() field = self._columns[column] value = "" if commit in self._modifications: modification = self._modifications[commit] value = modification[field] if value and field in ("children", "parents"): value = list(value) return value def set_merge(self, merge_state): """ Set the merge option, meaning that the committed and authored notions are merged. For instance, if the committed date is changed in the set_data() method, the authored date will be set with the same value. :param merge_state: Boolean, sets the merge option. """ self._merge = merge_state def set_data(self, index, value, ignore_history=False): """ Set the given value to the commit and the field determined by the index. :param index: The index of the commit and the field that should be modified. :param value: The value that will be assigned. """ if not 0 <= index.row() < len(self._commits): raise GfbiException("Invalid index") commit = self._commits[index.row()] column = index.column() field_name = self._columns[column] reference = None if field_name in TIME_FIELDS: if self.data(index) is not None: reference, tz = self.data(index) else: reference = self.data(index) # This is useless in the development version of GitPython # See https://github.com/gitpython-developers/GitPython/commit/096897123ab5d8b500024e63ca81b658f3cb93da git_python_precheck = (hasattr(reference, 'binsha') != hasattr(value, 'binsha')) if git_python_precheck or reference != value: self.set_field_data(commit, field_name, value) if not ignore_history: action = SetAction(index, reference, value) self._history[self._last_history_event].append(action) if self._merge: if field_name == "committed_date": self.set_field_data(commit, "authored_date", value) elif field_name == "authored_date": self.set_field_data(commit, "committed_date", value) elif field_name == "author_name": self.set_field_data(commit, "committer_name", value) elif field_name == "committer_name": self.set_field_data(commit, "author_name", value) elif field_name == "author_email": self.set_field_data(commit, "committer_email", value) elif field_name == "committer_email": self.set_field_data(commit, "author_email", value) def set_field_data(self, commit, field, value): """ This method is used in set_data() to assign the given value to the given field of the given commit. :param commit: The commit that will be modified. :param field: The field that will be modified. :param value: The value that will be assigned. """ if commit not in self._modifications: self._modifications[commit] = {} self._modifications[commit][field] = value def start_history_event(self): """ Start a new history event. If the current event isn't the last one, drop every event after the current event. """ while self._last_history_event < len(self._history) - 1: self._history.pop() self._last_history_event += 1 self._history.append([]) def undo_history(self): """ Reverts the history one event back. """ if self._last_history_event >= 0: for action in reversed(self._history[self._last_history_event]): action.undo(self) if self._last_history_event > -1: self._last_history_event -= 1 def redo_history(self): """ Replays the history one event forward. """ if self._last_history_event < len(self._history) - 1: self._last_history_event += 1 for action in self._history[self._last_history_event]: action.redo(self) def undelete_commit(self, commit, modifications): """ Remove a commit from the _deleted_commits list. """ self._deleted_commits.remove(commit) if modifications: self._modifications[commit] = modifications def insert_commit(self, row, commit, modifications): """ The parent commit is the previous commit in the history. """ self._commits.insert(row, commit) if modifications: self._modifications[commit] = modifications def insert_rows(self, position, rows): """ Inserts blank rows in the model. :param position: Position from where to insert. :param rows: Number of rows to insert. """ for i in xrange(rows): commit = DummyCommit() self._commits.insert(position, commit) self._modifications[commit] = {} for field in NAMES: self._modifications[commit][field] = None action = InsertAction(position, commit, self._modifications[commit]) self._history[self._last_history_event].append(action) def remove_rows(self, position, rows, ignore_history=False, really_remove=False): """ Removes rows from the model. :param position: Position from where to delete. :param rows: Number of rows to delete. """ for i in xrange(rows): if really_remove: commit = self._commits.pop(position) self._modifications.pop(commit) else: commit = self._commits[position + i] if not self.is_deleted(commit): self._deleted_commits.append(commit) if not ignore_history: modifications = None if self._modifications.has_key(commit): modifications = self._modifications[commit] action = RemoveAction(position, commit, modifications) self._history[self._last_history_event].append(action) def is_deleted(self, indexorcommit): """ If indexorcommit: * is an index, if the commit at index.row() is a deleted commit, return True. * is a commit, if the commit is a deleted commit, return True. """ if hasattr(indexorcommit, 'row'): if not 0 <= indexorcommit.row() < len(self._commits): raise GfbiException("Invalid index") commit = self._commits[indexorcommit.row()] else: commit = indexorcommit return commit in self._deleted_commits def is_inserted_commit(self, index): """ If the commit at index.row() is a DummyCommit, return True. """ commit = self._commits[index.row()] return isinstance(commit, DummyCommit) def is_modified(self, index): """ Returns True if the commit field determined by the index has been modified (if there is a corresponding entry in the _modifications dict). :param index: Index of the field of the commit. :return: True if the field of the commit is modified else False. """ commit = self._commits[index.row()] column = index.column() field_name = self._columns[column] mods = self._modifications if isinstance(commit, DummyCommit): return True if commit in mods and field_name in mods[commit]: return mods[commit][field_name] != self.orig_data(index) return False def commit_is_modified(self, commit): """ Returns True is one of the commit fields has been modified. """ row = self.row_of(commit) children_col = self.get_column("children") ignore_columns = set((children_col,)) for col in set(xrange(self.column_count())) - ignore_columns: index = Index(row, col) if self.is_modified(index) and not self.is_deleted(index): return True return False def write(self, log=True, force_committed_date=False, dont_populate=False): """ Start the git filter-branch command and therefore write the modifications stored in _modifications. :param log: Boolean, set to True to log the git command. :param force_committed_date: As the git way updates the committed author/date when cherry-picking, and since we offer to modify these values, we offer the user the choice to force the committed author/date or to let git update it. """ self._git_process = git_filter_rebase(self, log=log, force_committed_date=force_committed_date, dont_populate=dont_populate) # git_filter_branch_process(self, # directory=self._directory, # commits=self._commits, # modifications=self._modifications, # oldest_commit_parent=oldest_commit_parent, # log=log, script=script) self._git_process.start() def is_finished_writing(self): """ Returns False if the git command process isn't finished else True. """ if self._git_process is not None: return self._git_process.is_finished() return True def progress(self): """ Returns the git command process progress. """ if self._git_process is not None: return self._git_process.progress() return 0 def is_write_success(self): """ Returns True if the write process went through without failing. """ if self._git_process is not None: return self._git_process.is_success() else: return False def write_errors(self): """ Returns False if there wasn't any error, or returns the errors. """ if self._git_process is not None and self._git_process.errors(): return self._git_process.errors() else: return False def get_start_write_from(self): """ Returns the parents of the commits that are modified. """ modified_commits = tuple(self._modifications.keys()) if modified_commits in self._start_write_cache.keys(): return self._start_write_cache[modified_commits] parents = set() for commit in (set(self._modifications) | set(self._deleted_commits)): if self.commit_is_modified(commit) or self.is_deleted(commit): parents.add(commit) smaller_parent_set = set(parents) for parent in parents: # Check that parent isn't in any of the other parents history tree if parent not in smaller_parent_set: continue for _parent in parents - set([parent]): # Go down the history tree of _parent this_parent_parents = self.c_data(_parent, "parents") if _parent in smaller_parent_set and \ parent in self.all_parents(_parent): # Parent is present in the _parent history tree # That means _parent must be removed from # smaller_parent_set. smaller_parent_set.remove(_parent) if parents == set([]) and self.is_fake_model(): # Special case: no commit has really been modified, and this is a # fake model. We just need to update the top commit. smaller_parent_set = ([self._commits[0],]) self._start_write_cache[modified_commits] = smaller_parent_set return smaller_parent_set def all_parents(self, commit): """ Returns all the parents of a commit. """ parents = set() parents_to_look = set([commit,]) while parents_to_look: commit = parents_to_look.pop() for parent in self.c_data(commit, "parents"): yield parent parents_to_look.add(parent) def all_children(self, commits): """ Returns a set with all the children of the given commits. """ children = set() modified_commits = tuple(self._modifications.keys()) if modified_commits in self._children_cache.keys(): return self._children_cache[modified_commits] children_to_look = set(commits) while children_to_look: commit = children_to_look.pop() for child in self.c_data(commit, "children"): if not child in children: children_to_look.add(child) children.add(child) self._children_cache[modified_commits] = children return children def get_to_rewrite_count(self): """ Returns the number of commits to will be rewritten. That means the number of commit between HEAD and the oldest modified commit. """ start_from_commits = self.get_start_write_from() all_children = self.all_children(start_from_commits) return len(all_children) + len(start_from_commits) def erase_modifications(self): """ Erase all modifications: set _modified to {}. """ self._modifications = {} def reorder_commits(self, dates, times, weekdays): """ This method reorders the commits given specified timelapses and weekdays. """ timelapse = non_continuous_timelapse(dates, times, weekdays) ## Random method #delta = truc_truc.get_total_seconds() / (how_many_commits + 1) #max_error = delta / 2 # #time_cursor = 0 #for commit in xrange(how_many_commits): # time_cursor += delta # # The next lines sets the commit_time to time_cursor, plus or less # # an error # new_commit_time = time_cursor + int((random() * 2 - 1) *max_error) # Uniform method total_seconds = timelapse.get_total_seconds() distribution = [int(random() * total_seconds) for commit in xrange(len(self._commits))] distribution.sort() index = 0 for commit in self._commits: this_distribution = distribution[index] new_commit_time = timelapse.datetime_from_seconds( this_distribution) self.set_field_data(commit, "authored_date", int(mktime(new_commit_time.timetuple()))) self.set_field_data(commit, "committed_date", int(mktime(new_commit_time.timetuple()))) index += 1 def set_conflicting_commit(self, row): """ Sets the conflicting commit accordingly to the given row. """ self._conflicting_commit = self._commits[row] def get_conflicting_commit(self): """ Gets the conflicting commit. """ return self._conflicting_commit def get_conflicting_row(self): """ Returns the conflicting commit row. """ return self._commits.index(self._conflicting_commit) def is_conflicting_commit(self, row): """ Returns true is the given row points to the conflicting commit. """ if self._conflicting_commit is None: return False return self._conflicting_commit == self._commits[row] def set_unmerged_files(self, u_files): """ Sets the unmerged files as a dictionnary of : dict[filepath] = {"unmerged_content" : unmerged_content, "diff" : diff, "orig_content" : orig_content, "git_status" : git_status} where git_status is one of: DD : both deleted AU : added by us UD : deleted by them UA : added by them DU : deleted by us AA : both added UU : both modified orig_content is the content of the file at filepath before the merge conflict unmerged_content is the content of the file at filepath at the moment of the merge conflict and diff is the diff that should be applied by the conflicting commit. """ self._unmerged_files = u_files def get_unmerged_files(self): """ Returns the unmerged files after cherry-picking the conflicting commit (self._conflicting_commit). """ return self._unmerged_files def set_conflict_solutions(self, solutions): """ Sets the solutions for the current conflicting commit. :param solutions: This is a dictionnary like: solutions[filepath] = (action, custom_content) where filepath is the path of the unmerged file action is the action that should be taken after the conflict. Here is an explanation of what should happen for every action: delete : git rm filepath add : git add filepath add_custom : echo $custom_content > filepath git add filepath """ self._solutions[self._conflicting_commit] = solutions def get_conflict_solutions(self): """ Returns the solutions for every possible conflict of the model. """ return self._solutions def is_valid_branch_name(self, name): """ This allows us to check if the name is valid before setting it. """ try: validate_branch_name(name) except Exception, err: return False, err else:
class EditableGitModel(GitModel): """ This class represents the list of commits of the current branch of a given repository. This class is meant to be Qt-free so that it can be used in other ways than gitbuster. """ def __init__(self, directory=".", fake_branch_name="", from_commits=False): """ Initializes the model with the repository root directory. :param directory: Root directory of the git repository. """ if fake_branch_name: # This is an empy gitModel that will be filled with data from # another model self.orig_model = None else: self.orig_model = GitModel(directory=directory) self.init_attributes() GitModel.__init__(self, directory=directory, fake_branch_name=fake_branch_name, from_commits=from_commits) def init_attributes(self): """ These attributes should be resetted when populating the model. """ self._modifications = {} self._deleted_commits = [] self._merge = False self._history = [] self._last_history_event = -1 self._conflicting_commit = None self._unmerged_files = None self._solutions = {} self._new_branch_name = "" self._git_process = None self._start_write_cache = {} self._children_cache = {} def populate(self): """ Populates the model, by constructing a list of the commits of the current branch of the given repository. """ self.init_attributes() if not self.is_fake_model(): self.orig_model.populate() GitModel.populate(self) def set_current_branch(self, branch, force=False): """ Sets the model's current branch. :param branch: The desired branch to modelize. """ if self._changed_branch_once and not force: raise GfbiException("You shouldn't change the branch twice.") if self.is_fake_model(): # This is the moment after we wrote the model, the model is getting # real (not fake). self.orig_model = GitModel(directory=self._directory) self.orig_model.set_current_branch(branch, force=force) self._modifications = {} GitModel.set_current_branch(self, branch, force=force) def get_modifications(self): """ Returns the modified values dictionnary. """ return self._modifications def get_modified_count(self): """ Returns the number of modified commits. """ modified = set([ commit for commit in self._commits if self.commit_is_modified(commit) and not self.is_deleted(commit) ]) return len(modified) def get_deleted_count(self): """ Returns the number of deleted commits. """ deleted = set( [commit for commit in self._commits if self.is_deleted(commit)]) return len(deleted) def get_orig_model(self): """ Returns an unmodified GitModel. """ return self.orig_model def data(self, index): """ This method uses the index row to select the commit and the index column to select the field to return. :param index: The index of the wanted information. :return: Depending on the index column, one of the commit fields. """ if self.is_modified(index): return self.modified_data(index) else: return self.orig_data(index) def c_data(self, commit, field): """ This is a convenient method to access data using the commit and the column. """ row = self.row_of(commit) col = self.get_column(field) return self.data(Index(row, col)) def modified_data(self, index): commit = self._commits[index.row()] column = index.column() field = self._columns[column] value = "" if commit in self._modifications: modification = self._modifications[commit] value = modification[field] if value and field in ("children", "parents"): value = list(value) return value def set_merge(self, merge_state): """ Set the merge option, meaning that the committed and authored notions are merged. For instance, if the committed date is changed in the set_data() method, the authored date will be set with the same value. :param merge_state: Boolean, sets the merge option. """ self._merge = merge_state def set_data(self, index, value, ignore_history=False): """ Set the given value to the commit and the field determined by the index. :param index: The index of the commit and the field that should be modified. :param value: The value that will be assigned. """ if not 0 <= index.row() < len(self._commits): raise GfbiException("Invalid index") commit = self._commits[index.row()] column = index.column() field_name = self._columns[column] reference = None if field_name in TIME_FIELDS: if self.data(index) is not None: reference, tz = self.data(index) else: reference = self.data(index) # This is useless in the development version of GitPython # See https://github.com/gitpython-developers/GitPython/commit/096897123ab5d8b500024e63ca81b658f3cb93da git_python_precheck = (hasattr(reference, 'binsha') != hasattr( value, 'binsha')) if git_python_precheck or reference != value: self.set_field_data(commit, field_name, value) if not ignore_history: action = SetAction(index, reference, value) self._history[self._last_history_event].append(action) if self._merge: if field_name == "committed_date": self.set_field_data(commit, "authored_date", value) elif field_name == "authored_date": self.set_field_data(commit, "committed_date", value) elif field_name == "author_name": self.set_field_data(commit, "committer_name", value) elif field_name == "committer_name": self.set_field_data(commit, "author_name", value) elif field_name == "author_email": self.set_field_data(commit, "committer_email", value) elif field_name == "committer_email": self.set_field_data(commit, "author_email", value) def set_field_data(self, commit, field, value): """ This method is used in set_data() to assign the given value to the given field of the given commit. :param commit: The commit that will be modified. :param field: The field that will be modified. :param value: The value that will be assigned. """ if commit not in self._modifications: self._modifications[commit] = {} self._modifications[commit][field] = value def start_history_event(self): """ Start a new history event. If the current event isn't the last one, drop every event after the current event. """ while self._last_history_event < len(self._history) - 1: self._history.pop() self._last_history_event += 1 self._history.append([]) def undo_history(self): """ Reverts the history one event back. """ if self._last_history_event >= 0: for action in reversed(self._history[self._last_history_event]): action.undo(self) if self._last_history_event > -1: self._last_history_event -= 1 def redo_history(self): """ Replays the history one event forward. """ if self._last_history_event < len(self._history) - 1: self._last_history_event += 1 for action in self._history[self._last_history_event]: action.redo(self) def undelete_commit(self, commit, modifications): """ Remove a commit from the _deleted_commits list. """ self._deleted_commits.remove(commit) if modifications: self._modifications[commit] = modifications def insert_commit(self, row, commit, modifications): """ The parent commit is the previous commit in the history. """ self._commits.insert(row, commit) if modifications: self._modifications[commit] = modifications def insert_rows(self, position, rows): """ Inserts blank rows in the model. :param position: Position from where to insert. :param rows: Number of rows to insert. """ for i in xrange(rows): commit = DummyCommit() self._commits.insert(position, commit) self._modifications[commit] = {} for field in NAMES: self._modifications[commit][field] = None action = InsertAction(position, commit, self._modifications[commit]) self._history[self._last_history_event].append(action) def remove_rows(self, position, rows, ignore_history=False, really_remove=False): """ Removes rows from the model. :param position: Position from where to delete. :param rows: Number of rows to delete. """ for i in xrange(rows): if really_remove: commit = self._commits.pop(position) self._modifications.pop(commit) else: commit = self._commits[position + i] if not self.is_deleted(commit): self._deleted_commits.append(commit) if not ignore_history: modifications = None if self._modifications.has_key(commit): modifications = self._modifications[commit] action = RemoveAction(position, commit, modifications) self._history[self._last_history_event].append(action) def is_deleted(self, indexorcommit): """ If indexorcommit: * is an index, if the commit at index.row() is a deleted commit, return True. * is a commit, if the commit is a deleted commit, return True. """ if hasattr(indexorcommit, 'row'): if not 0 <= indexorcommit.row() < len(self._commits): raise GfbiException("Invalid index") commit = self._commits[indexorcommit.row()] else: commit = indexorcommit return commit in self._deleted_commits def is_inserted_commit(self, index): """ If the commit at index.row() is a DummyCommit, return True. """ commit = self._commits[index.row()] return isinstance(commit, DummyCommit) def is_modified(self, index): """ Returns True if the commit field determined by the index has been modified (if there is a corresponding entry in the _modifications dict). :param index: Index of the field of the commit. :return: True if the field of the commit is modified else False. """ commit = self._commits[index.row()] column = index.column() field_name = self._columns[column] mods = self._modifications if isinstance(commit, DummyCommit): return True if commit in mods and field_name in mods[commit]: return mods[commit][field_name] != self.orig_data(index) return False def commit_is_modified(self, commit): """ Returns True is one of the commit fields has been modified. """ row = self.row_of(commit) children_col = self.get_column("children") ignore_columns = set((children_col, )) for col in set(xrange(self.column_count())) - ignore_columns: index = Index(row, col) if self.is_modified(index) and not self.is_deleted(index): return True return False def write(self, log=True, force_committed_date=False, dont_populate=False): """ Start the git filter-branch command and therefore write the modifications stored in _modifications. :param log: Boolean, set to True to log the git command. :param force_committed_date: As the git way updates the committed author/date when cherry-picking, and since we offer to modify these values, we offer the user the choice to force the committed author/date or to let git update it. """ self._git_process = git_filter_rebase( self, log=log, force_committed_date=force_committed_date, dont_populate=dont_populate) # git_filter_branch_process(self, # directory=self._directory, # commits=self._commits, # modifications=self._modifications, # oldest_commit_parent=oldest_commit_parent, # log=log, script=script) self._git_process.start() def is_finished_writing(self): """ Returns False if the git command process isn't finished else True. """ if self._git_process is not None: return self._git_process.is_finished() return True def progress(self): """ Returns the git command process progress. """ if self._git_process is not None: return self._git_process.progress() return 0 def is_write_success(self): """ Returns True if the write process went through without failing. """ if self._git_process is not None: return self._git_process.is_success() else: return False def write_errors(self): """ Returns False if there wasn't any error, or returns the errors. """ if self._git_process is not None and self._git_process.errors(): return self._git_process.errors() else: return False def get_start_write_from(self): """ Returns the parents of the commits that are modified. """ modified_commits = tuple(self._modifications.keys()) if modified_commits in self._start_write_cache.keys(): return self._start_write_cache[modified_commits] parents = set() for commit in (set(self._modifications) | set(self._deleted_commits)): if self.commit_is_modified(commit) or self.is_deleted(commit): parents.add(commit) smaller_parent_set = set(parents) for parent in parents: # Check that parent isn't in any of the other parents history tree if parent not in smaller_parent_set: continue for _parent in parents - set([parent]): # Go down the history tree of _parent this_parent_parents = self.c_data(_parent, "parents") if _parent in smaller_parent_set and \ parent in self.all_parents(_parent): # Parent is present in the _parent history tree # That means _parent must be removed from # smaller_parent_set. smaller_parent_set.remove(_parent) if parents == set([]) and self.is_fake_model(): # Special case: no commit has really been modified, and this is a # fake model. We just need to update the top commit. smaller_parent_set = ([ self._commits[0], ]) self._start_write_cache[modified_commits] = smaller_parent_set return smaller_parent_set def all_parents(self, commit): """ Returns all the parents of a commit. """ parents = set() parents_to_look = set([ commit, ]) while parents_to_look: commit = parents_to_look.pop() for parent in self.c_data(commit, "parents"): yield parent parents_to_look.add(parent) def all_children(self, commits): """ Returns a set with all the children of the given commits. """ children = set() modified_commits = tuple(self._modifications.keys()) if modified_commits in self._children_cache.keys(): return self._children_cache[modified_commits] children_to_look = set(commits) while children_to_look: commit = children_to_look.pop() for child in self.c_data(commit, "children"): if not child in children: children_to_look.add(child) children.add(child) self._children_cache[modified_commits] = children return children def get_to_rewrite_count(self): """ Returns the number of commits to will be rewritten. That means the number of commit between HEAD and the oldest modified commit. """ start_from_commits = self.get_start_write_from() all_children = self.all_children(start_from_commits) return len(all_children) + len(start_from_commits) def erase_modifications(self): """ Erase all modifications: set _modified to {}. """ self._modifications = {} def reorder_commits(self, dates, times, weekdays): """ This method reorders the commits given specified timelapses and weekdays. """ timelapse = non_continuous_timelapse(dates, times, weekdays) ## Random method #delta = truc_truc.get_total_seconds() / (how_many_commits + 1) #max_error = delta / 2 # #time_cursor = 0 #for commit in xrange(how_many_commits): # time_cursor += delta # # The next lines sets the commit_time to time_cursor, plus or less # # an error # new_commit_time = time_cursor + int((random() * 2 - 1) *max_error) # Uniform method total_seconds = timelapse.get_total_seconds() distribution = [ int(random() * total_seconds) for commit in xrange(len(self._commits)) ] distribution.sort() index = 0 for commit in self._commits: this_distribution = distribution[index] new_commit_time = timelapse.datetime_from_seconds( this_distribution) self.set_field_data(commit, "authored_date", int(mktime(new_commit_time.timetuple()))) self.set_field_data(commit, "committed_date", int(mktime(new_commit_time.timetuple()))) index += 1 def set_conflicting_commit(self, row): """ Sets the conflicting commit accordingly to the given row. """ self._conflicting_commit = self._commits[row] def get_conflicting_commit(self): """ Gets the conflicting commit. """ return self._conflicting_commit def get_conflicting_row(self): """ Returns the conflicting commit row. """ return self._commits.index(self._conflicting_commit) def is_conflicting_commit(self, row): """ Returns true is the given row points to the conflicting commit. """ if self._conflicting_commit is None: return False return self._conflicting_commit == self._commits[row] def set_unmerged_files(self, u_files): """ Sets the unmerged files as a dictionnary of : dict[filepath] = {"unmerged_content" : unmerged_content, "diff" : diff, "orig_content" : orig_content, "git_status" : git_status} where git_status is one of: DD : both deleted AU : added by us UD : deleted by them UA : added by them DU : deleted by us AA : both added UU : both modified orig_content is the content of the file at filepath before the merge conflict unmerged_content is the content of the file at filepath at the moment of the merge conflict and diff is the diff that should be applied by the conflicting commit. """ self._unmerged_files = u_files def get_unmerged_files(self): """ Returns the unmerged files after cherry-picking the conflicting commit (self._conflicting_commit). """ return self._unmerged_files def set_conflict_solutions(self, solutions): """ Sets the solutions for the current conflicting commit. :param solutions: This is a dictionnary like: solutions[filepath] = (action, custom_content) where filepath is the path of the unmerged file action is the action that should be taken after the conflict. Here is an explanation of what should happen for every action: delete : git rm filepath add : git add filepath add_custom : echo $custom_content > filepath git add filepath """ self._solutions[self._conflicting_commit] = solutions def get_conflict_solutions(self): """ Returns the solutions for every possible conflict of the model. """ return self._solutions def is_valid_branch_name(self, name): """ This allows us to check if the name is valid before setting it. """ try: validate_branch_name(name) except Exception, err: return False, err else:
class QGitModel(QAbstractTableModel): def __init__(self, directory=".", model=None, fake_branch_name="", parent=None, remote_ref=None): """ Initializes the git model with the repository root directory. Here, we allow the QGitEditableModel to set the GitModel used. We do that in order to reduce the number of GitModel instanciated. :param directory: Root directory of the git repository. :param model: As given by QGitEditableModel.get_model().get_orig_model(). """ QAbstractTableModel.__init__(self, parent) if not model: self.git_model = GitModel(directory=directory, remote_ref=remote_ref) else: self.git_model = model self._filters = {} self._enabled_options = [] self._directory = directory self._parent = parent def populate(self): """ Populates the git model, see git_model.GitModel.populate for more infos. Moreover, it counts the number of filters that should be applied. """ self.git_model.populate() self.reset() def parent(self, index): #returns the parent of the model item with the given index. return QModelIndex() def data(self, index, role): """ Returns the data of the model. """ if not index.isValid() or not (0 <= index.row() < self.rowCount()): return QVariant() column = index.column() field_name = self.git_model.get_columns()[column] if role == Qt.DisplayRole: return self._data_display(index, field_name) elif role == Qt.EditRole: return self._data_edit(index, field_name) elif role == Qt.BackgroundColorRole: return self._data_background(index, field_name) elif role == Qt.ForegroundRole: return self._data_foreground(index, field_name) elif role == Qt.ToolTipRole: return self._data_tooltip(index, field_name) return QVariant() def _data_display(self, index, field_name): value = self.git_model.data(index) if field_name in TIME_FIELDS: _tmstmp, _tz = value _datetime = datetime.fromtimestamp(_tmstmp).replace(tzinfo=_tz) if "display_weekday" in self._enabled_options: date_format = "%d/%m/%Y %H:%M:%S (%a)" else: date_format = "%d/%m/%Y %H:%M:%S" return QVariant(_datetime.strftime(date_format)) elif field_name == "message": return QVariant(value.split("\n")[0]) elif field_name == "hexsha": return QVariant(value[:7]) return QVariant(value) def _data_edit(self, index, field_name): value = self.git_model.data(index) if field_name == "message": return QVariant(value) elif field_name in TIME_FIELDS or field_name in ("parents", "tree", "children"): return value return self._data_display(index, field_name) def _data_background(self, index, field_name): commits = self.git_model.get_commits() commit = commits[index.row()] if self.git_model.is_commit_pushed(commit): return QVariant(QColor(Qt.lightGray)) return QVariant() def _data_foreground(self, index, field_name): if "filters" in self._enabled_options and self.filter_score(index): return QVariant(QColor(Qt.red)) return QVariant() def _data_tooltip(self, index, field_name): value = self.git_model.data(index) if field_name == "hexsha": return QVariant(str(value)) elif field_name in TIME_FIELDS: _tmstmp, _tz = value _datetime = datetime.fromtimestamp(_tmstmp).replace(tzinfo=_tz) if "display_weekday" in self._enabled_options: date_format = "%Y-%m-%d %H:%M:%S %Z (%a)" else: date_format = "%Y-%m-%d %H:%M:%S %Z" return QVariant(_datetime.strftime(date_format)) elif field_name == "message": return QVariant(value) def filter_set(self, model_filter, value): """ Activates a filter/sets a filter value. :param model_filter: The filter to be set. :param value: The value of the filter. """ self._filters[model_filter] = value def filter_unset(self, model_filter): """ Deactivates a filter. :param model_filter: The filter to be deactivated. """ if model_filter in self._filters: self._filters.pop(model_filter) def enable_option(self, option): """ Enables a display option. :param option: The option to enable. """ if option not in self._enabled_options: self._enabled_options.append(option) def disable_option(self, option): """ Disables a display option. :param option: The option to disable. """ if option in self._enabled_options: self._enabled_options.pop(self._enabled_options.index(option)) def is_enabled(self, option): """ Return True if the display option is enabled. """ return option in self._enabled_options def date_match(self, item_date): """ Returns True if item_date matches the date filters. :param item_date: The date that will be evaluated. """ filters = self._filters filter_after_date = None filter_before_date = None if "afterDate" in filters: filter_after_date = filters["afterDate"] if "beforeDate" in filters: filter_before_date = filters["beforeDate"] if filter_after_date and filter_before_date: if filter_after_date < item_date < filter_before_date: return 1 elif (filter_after_date and filter_after_date < item_date): return 1 elif (filter_before_date and filter_before_date > item_date): return 1 return 0 def weekday_match(self, item_weekday): """ Returns True if item_weekday matches the weekday filters. :param item_weekday: The weekday that will be evaluated. """ filters = self._filters filter_after_weekday = None filter_before_weekday = None if "afterWeekday" in filters: filter_after_weekday = filters["afterWeekday"] + 1 if "beforeWeekday" in filters: filter_before_weekday = filters["beforeWeekday"] + 1 if filter_after_weekday and filter_before_weekday: if filter_after_weekday < item_weekday < filter_before_weekday: return 1 elif (filter_after_weekday and filter_after_weekday < item_weekday): return 1 elif (filter_before_weekday and filter_before_weekday > item_weekday): return 1 return 0 def time_match(self, item_time): """ Returns True if item_time matches the time filters. :param item_time: The time that will be evaluated. """ filters = self._filters filter_after_hour = None filter_before_hour = None if "afterHour" in filters: filter_after_hour = filters["afterHour"] if "beforeHour" in filters: filter_before_hour = filters["beforeHour"] if filter_after_hour and filter_before_hour: if filter_after_hour < item_time < filter_before_hour: return 1 elif (filter_after_hour and filter_after_hour < item_time): return 1 elif (filter_before_hour and filter_before_hour > item_time): return 1 return 0 def filter_score(self, index): """ Returns the number of filters matching the given index. :param index: The index of the item that will be checked against the filters. """ column = index.column() field_name = self.git_model.get_columns()[column] filters = self._filters if field_name in TIME_FIELDS: filters = self._filters timestamp, tz = self.git_model.data(index) _q_datetime = QDateTime() _q_datetime.setTime_t(timestamp) item_date = _q_datetime.date() item_weekday = item_date.dayOfWeek() item_time = _q_datetime.time() date_time_filters = ("afterWeekday", "beforeWeekday", "beforeDate", "afterDate", "beforeHour", "afterHour") has_date_time_filter = False for model_filter in filters: if model_filter in date_time_filters: has_date_time_filter = True if not has_date_time_filter: return 0 else: return self.date_match(item_date) + \ self.weekday_match(item_weekday) + \ self.time_match(item_time) elif field_name in ACTOR_FIELDS: if "nameEmail" in filters: regexp = filters["nameEmail"] value = self.git_model.data(index) if regexp.isValid() and regexp.indexIn(value) != -1: return 1 elif field_name in TEXT_FIELDS: if "message" in filters: regexp = filters["message"] commit_message = self.git_model.data(index) if regexp.isValid() and regexp.indexIn(commit_message) != -1: return 1 elif field_name == "hexsha": if "localOnly" in filters: commits = self.git_model.get_commits() commit = commits[index.row()] if not self.git_model.is_commit_pushed(commit): return 1 return 0 def headerData(self, section, orientation, role=Qt.DisplayRole): """ Returns the headers (qt model method). """ if role == Qt.TextAlignmentRole: if orientation == Qt.Horizontal: return QVariant(int(Qt.AlignLeft|Qt.AlignVCenter)) return QVariant(int(Qt.AlignRight|Qt.AlignVCenter)) if role != Qt.DisplayRole: return QVariant() if orientation == Qt.Horizontal: field_name = self.git_model.get_columns()[section] return QVariant(NAMES[field_name]) return QVariant(int(section + 1)) def flags(self, index): """ Returns the flags for the given index. """ # XXX varies if editable if not index.isValid(): return Qt.ItemIsEnabled elif self.is_first_commit(index): # The first commit can't be dragged and dropped return QAbstractTableModel.flags(self, index) return Qt.ItemFlags(QAbstractTableModel.flags(self, index)| Qt.ItemIsDragEnabled) def mimeData(self, indexes): mime_data = QMimeData() encoded_data = QByteArray() stream = QDataStream(encoded_data, QIODevice.WriteOnly) ref = self.get_current_branch() or self.get_remote_ref() for index in indexes: if index.isValid() and index.column() == 0: text = QString(ref.name + " ") text += QString(str(index.row())) stream.writeQString(text) mime_data.setData("application/vnd.text.list", encoded_data) return mime_data def name_to_display(self): """ Returns the name that should be displayed for this model. It can be the name of the current branch or the name of the remote reference. """ if self.get_remote_ref(): return self.get_remote_ref().name else: return self.get_current_branch().name def should_be_written(self): """ A QGitModel should never be written. This method is here to avoid model.should_be_written to fail when we don't know that the model is a QGitModel. """ return False # Beyond this point, abandon all hope of seeing anything more than "proxying # methods" (for instance, progress() calls git_model.progress()) def get_git_model(self): "See GitModel for more help." return self.git_model def rowCount(self, parent=QModelIndex()): "See GitModel for more help." return self.git_model.row_count() def columnCount(self, parent=QModelIndex()): "See GitModel for more help." return self.git_model.column_count() def get_branches(self): "See GitModel for more help." return self.git_model.get_branches() def get_current_branch(self): "See GitModel for more help." return self.git_model.get_current_branch() def set_current_branch(self, branch): "See GitModel for more help." return self.git_model.set_current_branch(branch) def get_remote_ref(self): "See GitModel for more help." return self.git_model.get_remote_ref() def reorder_commits(self, dates, time, weekdays): "See GitModel for more help." self.git_model.reorder_commits(dates, time, weekdays) self.reset() def row_of(self, commit): "See GitModel for more help." return self.git_model.row_of(commit) def get_columns(self): "See GitModel for more help." return self.git_model.get_columns() def get_old_branch_name(self): "See GitModel for more help." return self.git_model.get_old_branch_name() def is_first_commit(self, index): "See GitModel for more help." return self.git_model.is_first_commit(index)