class DataConnection(ProjectItem): def __init__(self, toolbox, project, logger, name, description, x, y, references=None): """Data Connection class. Args: toolbox (ToolboxUI): QMainWindow instance project (SpineToolboxProject): the project this item belongs to logger (LoggerInterface): a logger instance name (str): Object name description (str): Object description x (float): Initial X coordinate of item icon y (float): Initial Y coordinate of item icon references (list): a list of file paths """ super().__init__(name, description, x, y, project, logger) self._toolbox = toolbox self.reference_model = QStandardItemModel() # References to files self.data_model = QStandardItemModel( ) # Paths of project internal files. These are found in DC data directory self.datapackage_icon = QIcon(QPixmap(":/icons/datapkg.png")) self.data_dir_watcher = None # Populate references model if references is None: references = list() # Convert relative paths to absolute self.references = [ deserialize_path(r, self._project.project_dir) for r in references ] self.populate_reference_list(self.references) # Populate data (files) model data_files = self.data_files() self.populate_data_list(data_files) self.spine_datapackage_form = None def set_up(self): self.data_dir_watcher = QFileSystemWatcher(self) if os.path.isdir(self.data_dir): self.data_dir_watcher.addPath(self.data_dir) self.data_dir_watcher.directoryChanged.connect(self.refresh) @staticmethod def item_type(): """See base class.""" return ItemInfo.item_type() @staticmethod def item_category(): """See base class.""" return ItemInfo.item_category() def execution_item(self): """Creates DataConnection's execution counterpart.""" data_files = [ os.path.join(self.data_dir, f) for f in self.data_files() ] return ExecutableItem(self.name, self.file_references(), data_files, self._logger) def make_signal_handler_dict(self): """Returns a dictionary of all shared signals and their handlers. This is to enable simpler connecting and disconnecting.""" s = super().make_signal_handler_dict() # pylint: disable=unnecessary-lambda s[self._properties_ui.toolButton_dc_open_dir. clicked] = lambda checked=False: self.open_directory() s[self._properties_ui.toolButton_plus.clicked] = self.add_references s[self._properties_ui.toolButton_minus. clicked] = self.remove_references s[self._properties_ui.toolButton_add.clicked] = self.copy_to_project s[self._properties_ui.pushButton_datapackage. clicked] = self.show_spine_datapackage_form s[self._properties_ui.treeView_dc_references. doubleClicked] = self.open_reference s[self._properties_ui.treeView_dc_data. doubleClicked] = self.open_data_file s[self._properties_ui.treeView_dc_references. files_dropped] = self.add_files_to_references s[self._properties_ui.treeView_dc_data. files_dropped] = self.add_files_to_data_dir s[self.get_icon(). files_dropped_on_icon] = self.receive_files_dropped_on_icon s[self._properties_ui.treeView_dc_references. del_key_pressed] = lambda: self.remove_references() s[self._properties_ui.treeView_dc_data. del_key_pressed] = lambda: self.remove_files() return s def restore_selections(self): """Restore selections into shared widgets when this project item is selected.""" self._properties_ui.label_dc_name.setText(self.name) self._properties_ui.treeView_dc_references.setModel( self.reference_model) self._properties_ui.treeView_dc_data.setModel(self.data_model) @Slot("QVariant") def add_files_to_references(self, paths): """Add multiple file paths to reference list. Args: paths (list): A list of paths to files """ repeated_paths = [] new_paths = [] for path in paths: if any(os.path.samefile(path, ref) for ref in self.references): repeated_paths.append(path) else: new_paths.append(path) repeated_paths = ", ".join(repeated_paths) if repeated_paths: self._logger.msg_warning.emit( f"Reference to file(s) <b>{repeated_paths}</b> already available" ) if new_paths: self._toolbox.undo_stack.push( AddDCReferencesCommand(self, new_paths)) def do_add_files_to_references(self, paths): abspaths = [os.path.abspath(path) for path in paths] self.references.extend(abspaths) self.populate_reference_list(self.references) @Slot("QGraphicsItem", list) def receive_files_dropped_on_icon(self, icon, file_paths): """Called when files are dropped onto a data connection graphics item. If the item is this Data Connection's graphics item, add the files to data.""" if icon == self.get_icon(): self.add_files_to_data_dir(file_paths) @Slot("QVariant") def add_files_to_data_dir(self, file_paths): """Add files to data directory""" for file_path in file_paths: filename = os.path.split(file_path)[1] self._logger.msg.emit( f"Copying file <b>{filename}</b> to <b>{self.name}</b>") try: shutil.copy(file_path, self.data_dir) except OSError: self._logger.msg_error.emit("[OSError] Copying failed") return @Slot(bool) def add_references(self, checked=False): """Let user select references to files for this data connection.""" # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QFileDialog.getOpenFileNames(self._toolbox, "Add file references", self._project.project_dir, "*.*") file_paths = answer[0] if not file_paths: # Cancel button clicked return self.add_files_to_references(file_paths) @Slot(bool) def remove_references(self, checked=False): """Remove selected references from reference list. Do not remove anything if there are no references selected. """ indexes = self._properties_ui.treeView_dc_references.selectedIndexes() if not indexes: # Nothing selected self._logger.msg.emit("Please select references to remove") return references = [ind.data(Qt.DisplayRole) for ind in indexes] self._toolbox.undo_stack.push( RemoveDCReferencesCommand(self, references)) self._logger.msg.emit("Selected references removed") def do_remove_references(self, references): self.references = [ r for r in self.references if not any(os.path.samefile(r, ref) for ref in references) ] self.populate_reference_list(self.references) @Slot(bool) def copy_to_project(self, checked=False): """Copy selected file references to this Data Connection's data directory.""" selected_indexes = self._properties_ui.treeView_dc_references.selectedIndexes( ) if not selected_indexes: self._logger.msg_warning.emit("No files to copy") return for index in selected_indexes: file_path = self.reference_model.itemFromIndex(index).data( Qt.DisplayRole) if not os.path.exists(file_path): self._logger.msg_error.emit( f"File <b>{file_path}</b> does not exist") continue filename = os.path.split(file_path)[1] self._logger.msg.emit( f"Copying file <b>{filename}</b> to Data Connection <b>{self.name}</b>" ) try: shutil.copy(file_path, self.data_dir) except OSError: self._logger.msg_error.emit("[OSError] Copying failed") continue @Slot("QModelIndex") def open_reference(self, index): """Open reference in default program.""" if not index: return if not index.isValid(): logging.error("Index not valid") return reference = self.file_references()[index.row()] url = "file:///" + reference # noinspection PyTypeChecker, PyCallByClass, PyArgumentList res = open_url(url) if not res: self._logger.msg_error.emit( f"Failed to open reference:<b>{reference}</b>") @Slot("QModelIndex") def open_data_file(self, index): """Open data file in default program.""" if not index: return if not index.isValid(): logging.error("Index not valid") return data_file = self.data_files()[index.row()] url = "file:///" + os.path.join(self.data_dir, data_file) # noinspection PyTypeChecker, PyCallByClass, PyArgumentList res = open_url(url) if not res: self._logger.msg_error.emit( f"Opening file <b>{data_file}</b> failed") @busy_effect def show_spine_datapackage_form(self): """Show spine_datapackage_form widget.""" if self.spine_datapackage_form: if self.spine_datapackage_form.windowState() & Qt.WindowMinimized: # Remove minimized status and restore window with the previous state (maximized/normal state) self.spine_datapackage_form.setWindowState( self.spine_datapackage_form.windowState() & ~Qt.WindowMinimized | Qt.WindowActive) self.spine_datapackage_form.activateWindow() else: self.spine_datapackage_form.raise_() return self.spine_datapackage_form = SpineDatapackageWidget(self) self.spine_datapackage_form.destroyed.connect( self.datapackage_form_destroyed) self.spine_datapackage_form.show() @Slot() def datapackage_form_destroyed(self): """Notify a connection that datapackage form has been destroyed.""" self.spine_datapackage_form = None def make_new_file(self): """Create a new blank file to this Data Connections data directory.""" msg = "File name" # noinspection PyCallByClass, PyTypeChecker, PyArgumentList answer = QInputDialog.getText(self._toolbox, "Create new file", msg, flags=Qt.WindowTitleHint | Qt.WindowCloseButtonHint) file_name = answer[0] if not file_name.strip(): return # Check that file name has no invalid chars if any(True for x in file_name if x in INVALID_FILENAME_CHARS): msg = f"File name <b>{file_name}</b> contains invalid characters." self._logger.information_box.emit("Creating file failed", msg) return file_path = os.path.join(self.data_dir, file_name) if os.path.exists(file_path): msg = f"File <b>{file_name}</b> already exists." self._logger.information_box.emit("Creating file failed", msg) return try: with open(file_path, "w"): self._logger.msg.emit( f"File <b>{file_name}</b> created to Data Connection <b>{self.name}</b>" ) except OSError: msg = "Please check directory permissions." self._logger.information_box.emit("Creating file failed", msg) return def remove_files(self): """Remove selected files from data directory.""" indexes = self._properties_ui.treeView_dc_data.selectedIndexes() if not indexes: # Nothing selected self._logger.msg.emit("Please select files to remove") return file_list = list() for index in indexes: file_at_index = self.data_model.itemFromIndex(index).data( Qt.DisplayRole) file_list.append(file_at_index) files = "\n".join(file_list) msg = ( "The following files will be removed permanently from the project\n\n" "{0}\n\n" "Are you sure?".format(files)) title = "Remove {0} File(s)".format(len(file_list)) message_box = QMessageBox(QMessageBox.Question, title, msg, QMessageBox.Ok | QMessageBox.Cancel, parent=self._toolbox) message_box.button(QMessageBox.Ok).setText("Remove Files") answer = message_box.exec_() if answer == QMessageBox.Cancel: return for filename in file_list: path_to_remove = os.path.join(self.data_dir, filename) try: os.remove(path_to_remove) self._logger.msg.emit(f"File <b>{path_to_remove}</b> removed") except OSError: self._logger.msg_error.emit( f"Removing file {path_to_remove} failed.\nCheck permissions." ) return def file_references(self): """Returns a list of paths to files that are in this item as references.""" return self.references def data_files(self): """Returns a list of files that are in the data directory.""" if not os.path.isdir(self.data_dir): return [] files = list() with os.scandir(self.data_dir) as scan_iterator: for entry in scan_iterator: if entry.is_file(): files.append(entry.path) return files @Slot("QString") def refresh(self, _=None): """Refresh data files in Data Connection Properties. NOTE: Might lead to performance issues.""" d = self.data_files() self.populate_data_list(d) def populate_reference_list(self, items, emit_item_changed=True): """List file references in QTreeView. If items is None or empty list, model is cleared. """ self.reference_model.clear() self.reference_model.setHorizontalHeaderItem( 0, QStandardItem("References")) # Add header if items is not None: for item in items: qitem = QStandardItem(item) qitem.setFlags(~Qt.ItemIsEditable) qitem.setData(item, Qt.ToolTipRole) qitem.setData( self._toolbox.style().standardIcon(QStyle.SP_FileLinkIcon), Qt.DecorationRole) self.reference_model.appendRow(qitem) if emit_item_changed: self.item_changed.emit() def populate_data_list(self, items): """List project internal data (files) in QTreeView. If items is None or empty list, model is cleared. """ self.data_model.clear() self.data_model.setHorizontalHeaderItem( 0, QStandardItem("Data")) # Add header if items is not None: for item in items: qitem = QStandardItem(item) qitem.setFlags(~Qt.ItemIsEditable) if item == 'datapackage.json': qitem.setData(self.datapackage_icon, Qt.DecorationRole) else: qitem.setData(QFileIconProvider().icon(QFileInfo(item)), Qt.DecorationRole) full_path = os.path.join(self.data_dir, item) # For drag and drop qitem.setData(full_path, Qt.UserRole) self.data_model.appendRow(qitem) self.item_changed.emit() def update_name_label(self): """Update Data Connection tab name label. Used only when renaming project items.""" self._properties_ui.label_dc_name.setText(self.name) def resources_for_direct_successors(self): """see base class""" refs = self.file_references() f_list = [os.path.join(self.data_dir, f) for f in self.data_files()] resources = [ ProjectItemResource(self, "file", url=pathlib.Path(ref).as_uri()) for ref in refs + f_list ] return resources def _do_handle_dag_changed(self, resources): """See base class.""" if not self.file_references() and not self.data_files(): self.add_notification( "This Data Connection does not have any references or data. " "Add some in the Data Connection Properties panel.") def item_dict(self): """Returns a dictionary corresponding to this item.""" d = super().item_dict() # Convert paths to relative before saving d["references"] = [ serialize_path(f, self._project.project_dir) for f in self.file_references() ] return d def rename(self, new_name): """Rename this item. Args: new_name (str): New name Returns: bool: True if renaming succeeded, False otherwise """ dirs = self.data_dir_watcher.directories() if dirs: self.data_dir_watcher.removePaths(dirs) if not super().rename(new_name): self.data_dir_watcher.addPaths(dirs) return False self.data_dir_watcher.addPath(self.data_dir) self.refresh() return True def tear_down(self): """Tears down this item. Called by toolbox just before closing. Closes the SpineDatapackageWidget instances opened.""" if self.spine_datapackage_form: self.spine_datapackage_form.close() watched_paths = self.data_dir_watcher.directories() if watched_paths: self.data_dir_watcher.removePaths(watched_paths) self.data_dir_watcher.deleteLater() def notify_destination(self, source_item): """See base class.""" if source_item.item_type() == "Tool": self._logger.msg.emit( f"Link established. Tool <b>{source_item.name}</b> output files will be " f"passed as references to item <b>{self.name}</b> after execution." ) elif source_item.item_type() in ["Data Store", "Importer"]: # Does this type of link do anything? self._logger.msg.emit("Link established.") else: super().notify_destination(source_item) @staticmethod def default_name_prefix(): """See base class.""" return "Data Connection"
class Pqgit(QMainWindow): """ main class / entry point """ def __init__(self): super().__init__() self.setAttribute( Qt.WA_DeleteOnClose ) # let Qt delete stuff before the python garbage-collector gets to work self.repo = None self.branches_model = None # instantiate main window self.ui = ui.Ui_MainWindow() self.ui.setupUi(self) self.fs_watch = QFileSystemWatcher(self) self.fs_watch.fileChanged.connect(self.on_file_changed) self.fs_watch.directoryChanged.connect(self.on_dir_changed) self.settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'pqgit', 'config') # for comparison self.new_c_id, self.old_c_id = None, None # window icon cwd = os.path.dirname(os.path.realpath(__file__)) self.setWindowIcon(QIcon(os.path.join(cwd, 'Git-Icon-White.png'))) self.setWindowTitle('pqgit') # size and position self.move(self.settings.value('w/pos', QPoint(200, 200))) self.resize(self.settings.value('w/size', QSize(1000, 1000))) self.ui.hist_splitter.setSizes([ int(s) for s in self.settings.value('w/hist_splitter', [720, 360]) ]) self.ui.cinf_splitter.setSizes([ int(s) for s in self.settings.value('w/cinf_splitter', [360, 360]) ]) self.ui.diff_splitter.setSizes([ int(s) for s in self.settings.value('w/diff_splitter', [150, 1200, 230]) ]) # open repo dir open_shortcut = QShortcut(QKeySequence('Ctrl+O'), self) open_shortcut.activated.connect(self.open_dir) # set-up ui self.branches_model = BranchesModel() self.ui.tvBranches.setModel(self.branches_model) self.ui.tvBranches.selectionModel().selectionChanged.connect( self.branches_selection_changed) self.ui.tvBranches.resizeColumnsToContents() self.history_model = HistoryModel() self.ui.tvHistory.setModel(self.history_model) self.ui.tvHistory.selectionModel().selectionChanged.connect( self.history_selection_changed) self.files_model = FilesModel() self.ui.tvFiles.setModel(self.files_model) self.ui.tvFiles.selectionModel().selectionChanged.connect( self.files_selection_changed) self.ui.tvFiles.doubleClicked.connect(self.on_file_doubleclicked) for view in (self.ui.tvBranches, self.ui.tvHistory, self.ui.tvFiles): view.horizontalHeader().setSectionResizeMode( 1, QHeaderView.Stretch) view.setSelectionBehavior(QAbstractItemView.SelectRows) view.setShowGrid(False) view.verticalHeader().setDefaultSectionSize( QApplication.font().pointSize() + 2) view.verticalHeader().hide() self.ui.teDiff.setFont(QFont('Monospace')) self.difftools = [] timer = QTimer(self) timer.timeout.connect(self.on_timer) timer.start(5000) self.dir_name = self.settings.value('last_opened_repo', None) try: pygit2.Repository(self.dir_name) except Exception: #pylint: disable=broad-except self.open_dir() return self.open_repo() def open_dir(self): """ show open dir dialog and open repo """ last_dir = self.settings.value('last_fileopen_dir', '') fd = QFileDialog(self, 'Open .git', last_dir) fd.setFileMode(QFileDialog.DirectoryOnly) fd.setFilter( QDir.Filters(QDir.Dirs | QDir.Hidden | QDir.NoDot | QDir.NoDotDot)) while True: if not fd.exec(): return self.dir_name = fd.selectedFiles()[0] parent = os.path.dirname(self.dir_name) self.settings.setValue('last_fileopen_dir', parent) self.settings.setValue('last_opened_repo', self.dir_name) try: pygit2.Repository(self.dir_name) break except pygit2.GitError: QMessageBox(self, text='Cannot open repo: ' + self.dir_name).exec() self.open_repo() def open_repo(self): """ called either on start or after open dialog """ self.setWindowTitle(f'{self.dir_name} - pqgit ({VERSION})') self.repo = pygit2.Repository(self.dir_name) # remove existing files and folder from watch if self.fs_watch.files(): self.fs_watch.removePaths(self.fs_watch.files()) if self.fs_watch.directories(): self.fs_watch.removePaths(self.fs_watch.directories()) wd = self.repo.workdir self.fs_watch.addPath(wd) # get head tree for list of files in repo target = self.repo.head.target last_commit = self.repo[target] tree_id = last_commit.tree_id tree = self.repo[tree_id] # add those files and folder to watch self.fs_watch.addPaths([wd + o[0] for o in parse_tree_rec(tree, True)]) # get files/folders not in repo from status self.fs_watch.addPaths([ wd + p for p, f in self.repo.status().items() if GIT_STATUS[f] != 'I' ]) # (doesn't matter some are in both lists, already monitored ones will not be added by Qt) # local branches branches = [] selected_branch_row = 0 for idx, b_str in enumerate(self.repo.branches.local): b = self.repo.branches[b_str] if b.is_checked_out(): selected_branch_row = idx branches.append( Branch(name=b.branch_name, ref=b.name, c_o=b.is_checked_out())) # tags regex = re.compile('^refs/tags') tags = list(filter(regex.match, self.repo.listall_references())) branches += [Branch(name=t[10:], ref=t, c_o=False) for t in tags] self.branches_model.update(branches) idx1 = self.branches_model.index(selected_branch_row, 0) idx2 = self.branches_model.index(selected_branch_row, self.branches_model.columnCount() - 1) self.ui.tvBranches.selectionModel().select(QItemSelection(idx1, idx2), QItemSelectionModel.Select) self.ui.tvHistory.resizeColumnsToContents() def on_timer(self): """ poll opened diff tools (like meld) and close temp files when finished """ for dt in self.difftools: if subprocess.Popen.poll(dt.proc) is not None: if dt.old_f: dt.old_f.close() if dt.new_f: dt.new_f.close() dt.running = False self.difftools[:] = [dt for dt in self.difftools if dt.running] def on_file_changed(self, path): """ existing files edited """ patch = self.files_model.patches[ self.ui.tvFiles.selectionModel().selectedRows()[0].row()] if self.repo.workdir + patch.path == path: self.files_selection_changed() def on_dir_changed(self, path): """ file added/deleted; refresh history to show it in 'working' """ # remember history selection history_ids = [] for idx in self.ui.tvHistory.selectionModel().selectedRows(): history_ids.append(self.history_model.commits[idx.row()].id) bak_path = self.files_model.patches[ self.ui.tvFiles.selectionModel().selectedRows()[0].row()].path self.refresh_history() # restore history selection for i in history_ids: for row, c in enumerate(self.history_model.commits): if c.id == i: idx1 = self.history_model.index(row, 0) idx2 = self.history_model.index( row, self.history_model.columnCount() - 1) self.ui.tvHistory.selectionModel().select( QItemSelection(idx1, idx2), QItemSelectionModel.Select) # restore file selection if not bak_path: return for row, patch in enumerate(self.files_model.patches): if patch.path == bak_path: idx1 = self.files_model.index(row, 0) idx2 = self.files_model.index( row, self.files_model.columnCount() - 1) self.ui.tvFiles.selectionModel().select( QItemSelection(idx1, idx2), QItemSelectionModel.Select) break def refresh_history(self): """ called and branch check-out (which is also called during start-up) to populate commit log """ commits = [] # working directory status = self.repo.status() if len(status.items()) > 0: commits.append(Commit('working', 'working', None, None, None, None)) for c in self.repo.walk(self.repo.head.target, pygit2.GIT_SORT_TOPOLOGICAL): commit = Commit(id=c.id.hex, tree_id=c.tree_id.hex, author=c.author, dt=c.commit_time, dt_offs=c.commit_time_offset, message=c.message.strip()) commits.append(commit) self.history_model.update(commits) self.ui.tvHistory.resizeColumnsToContents() def branches_selection_changed(self): """ checkout selected branch """ selected_row = self.ui.tvBranches.selectionModel().selectedRows( )[0].row() self.repo.checkout(self.branches_model.branches[selected_row].ref, strategy=pygit2.GIT_CHECKOUT_SAFE) self.refresh_history() def on_file_doubleclicked(self, index): """ get files contents for revisions and start diff tool """ patch = self.files_model.patches[index.row()] if not patch.old_file_id: msg_box = QMessageBox(self) msg_box.setText("Nothing to compare to.") msg_box.exec() return old_f = tempfile.NamedTemporaryFile( prefix=f'old_{self.old_c_id[:7]}__') old_f.write(self.repo[patch.old_file_id].data) old_f.flush() new_f = None if patch.new_file_id: # compare 2 revisions new_f = tempfile.NamedTemporaryFile( prefix=f'new_{self.new_c_id[:7]}__') new_f.write(self.repo[patch.new_file_id].data) new_f.flush() new_f_name = new_f.name else: # compare some revision with working copy new_f_name = self.repo.workdir + patch.path.strip() proc = subprocess.Popen( [self.settings.value('diff_tool', 'meld'), old_f.name, new_f_name]) self.difftools.append(Proc(proc, old_f, new_f, True)) def history_selection_changed(self, selected): """ docstring """ self.ui.teDiff.setText('') self.new_c_id, self.old_c_id = None, None selection_model = self.ui.tvHistory.selectionModel() selected_rows = selection_model.selectedRows() self.ui.teCommit.setPlainText('') commit = None fst_tid, fst_obj = None, None snd_tid, snd_obj = None, None if len(selected_rows) < 1: # nothing to do return if len(selected_rows) > 2: # don't allow more than 2 selected lines selection_model.select(selected, QItemSelectionModel.Deselect) return if len(selected_rows) == 1: # single revision selected commit = self.history_model.commits[selected_rows[0].row()] fst_tid = commit.tree_id if selected_rows[0].row() + 1 < self.history_model.rowCount(): # there is a parent, get it's id to compare to it snd_commit = self.history_model.commits[selected_rows[0].row() + 1] snd_tid = snd_commit.tree_id self.new_c_id = commit.id self.old_c_id = snd_commit.id # set commit details in view if commit.tree_id != 'working': text = 'Commit: ' + commit.id + '\n\n' text += 'Author: ' + commit.author.name + ' <' + commit.author.email + '>\n\n' text += commit.message + '\n' self.ui.teCommit.setPlainText(text) else: # 2 revisions selected fst_row, snd_row = tuple( sorted([selected_rows[0].row(), selected_rows[1].row()])) commit = self.history_model.commits[fst_row] fst_tid = commit.tree_id snd_commit = self.history_model.commits[snd_row] snd_tid = snd_commit.tree_id self.new_c_id = commit.id self.old_c_id = snd_commit.id if fst_tid != 'working': fst_obj = self.repo.revparse_single(fst_tid) if snd_tid: snd_obj = self.repo.revparse_single(snd_tid) diff = None if fst_tid == 'working': # diff for working directory only shows... some files; get them anyway, then insert the ones from status diff = self.repo.diff( snd_obj, None) # regardless of snd_obj being something or None patches = [ Patch( p.delta.new_file.path.strip(), # p.delta.status_char(), None, # p.delta.new_file.id.hex is 'some' id, but it's somehow not ok... p.delta.old_file.id.hex if p.delta.old_file.id.hex.find('00000') < 0 else None, ) for p in diff ] inserted = [p.delta.new_file.path for p in diff] status = self.repo.status() for path, flags in status.items(): if path not in inserted: patches.append( Patch(path.strip(), GIT_STATUS[flags], None, None)) elif snd_obj: diff = self.repo.diff(snd_obj, fst_obj) patches = [ Patch( p.delta.new_file.path.strip(), # p.delta.status_char(), p.delta.new_file.id.hex if p.delta.new_file.id.hex.find('00000') < 0 else None, p.delta.old_file.id.hex if p.delta.old_file.id.hex.find('00000') < 0 else None, ) for p in diff ] else: # initial revision patches = [ Patch(o[0], 'A', o[1], None) for o in parse_tree_rec(fst_obj) ] patches = sorted(patches, key=lambda p: p.path) self.files_model.update(patches) self.ui.tvFiles.resizeColumnsToContents() def files_selection_changed(self): """ show diff (or file content for new, ignored, ... files) """ patch = self.files_model.patches[ self.ui.tvFiles.selectionModel().selectedRows()[0].row()] nf_data, of_data = None, None # new_file, old_file if patch.new_file_id: nf_data = self.repo[patch.new_file_id].data.decode('utf-8') if patch.old_file_id: of_data = self.repo[patch.old_file_id].data.decode('utf-8') if nf_data and of_data: html = _html_diff.make_file( fromlines=nf_data.splitlines(), # tolines=of_data.splitlines(), fromdesc=f'old ({self.old_c_id[:7]})', todesc=f'new ({self.new_c_id[:7]})', context=True) self.ui.teDiff.setHtml(html) elif nf_data: self.ui.teDiff.setText(nf_data) elif of_data: if patch.status == 'M': # this should be working directory compared to something else with open(self.repo.workdir + patch.path.strip()) as f: nf_data = f.read() html = _html_diff.make_file( fromlines=nf_data.splitlines(), tolines=of_data.splitlines(), fromdesc=f'old ({self.old_c_id[:7]})', todesc=f'new ({self.new_c_id[:7]})', context=True) self.ui.teDiff.setHtml(html) else: self.ui.teDiff.setText(of_data) else: with open(self.repo.workdir + patch.path.strip()) as f: self.ui.teDiff.setPlainText(f.read()) self.ui.diff_groupbox.setTitle( 'Diff' if nf_data and of_data else 'File') def closeEvent(self, event): # pylint: disable=invalid-name, no-self-use """ event handler for window closing; save settings """ del event self.settings.setValue('w/pos', self.pos()) self.settings.setValue('w/size', self.size()) self.settings.setValue('w/hist_splitter', self.ui.hist_splitter.sizes()) self.settings.setValue('w/cinf_splitter', self.ui.cinf_splitter.sizes()) self.settings.setValue('w/diff_splitter', self.ui.diff_splitter.sizes()) # delete any left temp files self.on_timer()