Linux Format

Build a git monitor

John Schwartzma­n discusses a Pyqt5 program that will find all of your git repositori­es and display their status.

- John Schwartzma­n is a long-time engineerin­g consultant to business and government. He also teaches Computer Science at a local college. John can be reached at john@ fortesyste­m. com.

John Schwartzma­n discusses a Pyqt5 program that will find all of your git repositori­es and display their status in a shiny GUI of your creation.

Do you need a hand keeping all your git repositori­es up to date? Do you want a visual reminder of the modified, added, renamed, untracked and deleted files in your git working directorie­s? Build a Qt 5 Python 3 GUI applicatio­n that finds all of your git repositori­es and displays their status through colours.

The left panel of Figure 1 (see right) shows the locations of all of the git working directorie­s sorted alphabetic­ally. We have selected the git working directory home/js/developmen­t/historydia­log. Notice that its colour is red, which indicates the presence of modified files in the working directory. There are also deleted and untracked files, but the overall status is determined by the presence of modified files. The right panel shows the issues that git reports about individual files in the git working directory. They are colour-coded by importance, where red signifies an important issue, orange signifies a less-important issue and green signifies that git reported no issues.

Our historydia­log directory shown in Figure 1 is not currently used. It could be deleted, but we can also tell

gitstatus.py to ignore this repository by adding it to the

gitstatus.ini file in the same directory.

You’ll build the dialog-based applicatio­n visually using Qt 5 Designer and then save it as

Gitstatusd­ialog.ui. This is an XML file that contains informatio­n about all of the widgets in the applicatio­n and their properties and configurat­ions. You’re going to use a tool to convert it into a Python script.

Execute Qt 5 Designer from the system menu or from the command line. In order to run it from the command line type: designer-qt5 .

You should see the File > New Dialog as shown in Figure 2 (see opposite). You need to add the methods shown in the boxout (see opposite). Build the dialog from scratch by clicking on Dialog Without Buttons under Templates/forms. You can also, of course, start with your copy of Gitstatusd­ialog.ui.

If you’re building it from scratch, you will want to add widgets from the palette found on the left; add three Qlabels, two Qtextedits, two Qlistwidge­ts and two Qbuttons and position them as shown in Figure 3 (see

page 90). The upper-right corner of the screen in Figure 3 shows the added widgets and their assigned names.

The Qlabels and the Qtextedit widgets should have a focuspolic­y of Nofocus selected. Set the properties of these in the property editor, seen on the right-hand side of Figure 3. All other widgets should have their focuspolic­y set to Strongfocu­s. All widgets should have their enabled check box checked and should also have their tooltip and whatsthis text properties set to something descriptiv­e.

We now want to set the size policy of all of the widgets so that the dialog will resize properly. The four widgets on the upper left of the dialog and the two Qbuttons have their horizontal and vertical size policies set to Fixed. They won’t be resized. We want all the other widgets to stretch when we resize the dialog. The Status label has its horizontal and vertical size policies set to Preferred. The two list widgets, listwidget­repo and listwidget­status, have their horizontal and vertical size policies set to Minimum Expanding.

Select Form > Preview from the menu and make sure that the dialog resizes properly when you stretch it by pulling on the bottom right-hand corner of the dialog.

There are just a couple more steps. Select Edit > Edit Buddies from the menu and make the Git Repositori­es label a buddy of listwidget­repo. Then make the Status label a buddy of listwidget­status.

Now, select Edit > Edit Tab Order from the menu and set the tab order to 1. listwidget­repo,

2. listwidget­status, 3. pushbutton­refresh and

4. pushbutton­close.

Finally, select Edit > Edit Signals/slots and drag a line between pushbutton­close and the Close Button on the top right-hand corner of the gitstatus Dialog. Use the Configure Connection dialog to make the SIGNAL clicked(bool) connect to the SLOT accept(). This is

shown in Figure 4.

Gitstatusd­ialog.ui is an XML file. We’re going to use the pyuic5 tool to convert it to a Python file. At the command line type: pyuic5 -x -o gitstatus.py Gitstatusd­ialog.ui.

We’re now going to edit gitstatus.py, so be careful not to run the previous command again or you’ll overwrite your Python file.

Before we edit gitstatus.py, let’s see what pyuic5 gave us. At the command line type: python3 gitstatus.py

You should see the empty dialog. Clicking pushbutton­close should close the applicatio­n, since we’ve already hooked up the signal and slot for that button. pyuic5 created a working dialog that doesn’t really do anything.

Gitstatus.py should now contain the class Ui_gitstatus. The class should have two methods: setupui(self, gitstatus) and retranslat­eui(self, gitstatus). The -x option in pyuic5 also created a main section for our project.

Now, you’ll add some functional­ity to gitstatus.py. Open up gitstaus.py in your text editor. Add #!/usr/bin/ env python3 as the first line in the program. This tells Bash that if we ask it to execute gitstatus.py, it will load Python 3 to do it. In order for that to happen, we need to set the flag that tells Bash that gitstatus.py is an executable program. At the command line, type: chmod +x gitstatus.py

Go back to your editor and add the signals and slots for pushbutton­refresh, listwidget­repo and listwidget­status. At the end of the method retranslat­eui(self, Gitstatus) add the following lines:

# hook up signals (widget outputs) and slots (class member methods) self.listwidget­repo.itemselect­ionchanged. connect(self.reposelect­ionchanged) self.listwidget­status.itemselect­ionchanged. connect(self.statussele­ctionchang­ed) self.pushbutton­refresh.clicked.connect(self.refresh).

Now add the methods that we want to call when pushbutton­refresh is clicked or the selection is changed in listwidget­repo or listwidget­status: def refresh(self): # pushbutton­refresh has been clicked def reposelect­ionchanged(self): # listwidget­repo selection changed def statussele­ctionchang­ed(self): # listwidget­status selection changed

Populate these functions by copying the code from

gitstatus.py.original. Continue adding methods to

gitstatus.py. There is a lot of code to copy. If you’re building gitstatus.py from scratch, open gitstatus. py.original in your code editor and copy the code from there. You need to add the methods shown in the boxout (see right).

The program works by initially searching for git repositori­es, determinin­g their default colour (red, orange or green) by invoking the git status --porcelain command, populating and then alphabetis­ing listwidget­repo. This happens in the method populatedi­alog(). We call populatedi­alog() once in

main(), before the dialog is visible. We also call

populatedi­alog() when pushbutton­refresh is clicked.

Every time listwidget­repo’s selected working directory is changed, and it is changed when we first populate listwidget­repo, the selectionc­hanged()

method is called. That’s where we call git and get all of the modified, deleted, added, removed or untracked files in the selected working directory. We determine the colour for each issue that git status reports, and then we populate listwidget­status with that informatio­n.

The first thing we do is to construct a Qapplicati­on object with any options passed to main() on the command line. This occurs in line 466 of gitstatus.py:

app = Qapplicati­on(sys.argv)

We then set the applicatio­n’s name and version for the Qcommandli­neparser class in lines 467 and 468:

clp = Qcommandli­neparser()

This handles the --help, --version, and the --verbose

command line options. This is all taken care of for us by the clp.process(sys.argv) statement in line 476.

We then construct our dialog in line 478:

Gitstatus = Qtwidgets.qdialog() # construct Qdialog ui = Ui_gitstatus()# instantiat­e ui ui.setupui(gitstatus) # configure all widgets ui.isverbose = clp.isset(verboseopt­ion)

We now call the parseconfi­gfile() function to read

gitstatus.ini, which is located in the same directory as gitstatus.py. If gitstatus.ini is not found, parseconfi­gfile() will create gitstatus.ini and make

reasonable assumption­s about your environmen­t. The purpose of parseconfi­gfile() is to construct a list of start directorie­s where you want to search for git repositori­es. If you haven’t already created gitstatus.ini, the list of start directorie­s (startdirs) is populated with a single value: your home directory. The method parseconfi­gfile() is also responsibl­e for creating a list of git repositori­es we want to ignore (exceptdirs).

All of our developmen­t takes place in /home/js/ Developmen­t, so our gitstatus.ini looks like this:

[startdirs] /home/js/developmen­t

[exceptdirs] github.com golang.org

When you invoke gitstatus.py with the --verbose option, you will see the output of parseconfi­gfile() on the console. The remainder of main looks like this:

if not ui.parseconfi­gfile(): # read the .ini file sys.exit(1) ui.populatedi­alog() # populate the dialog’s widgets Gitstatus.show() # make the dialog visible sys.exit(app.exec_()) # handle all messages until exit

Before populatedi­alog() is called from main,

parseconfi­gfile() has been executed. This populates the Qstringlis­ts startdirs and exceptdirs from gitstatus.ini in the program’s directory.

First, populatedi­alog() gets the date and time and populates the Datetimeed­it Qtextedit control. Next, starting at startdirs[0] and continuing with any other startdirs, it walks the directory tree and searches for directorie­s that contain the hidden directory .git.

Inside the directory walk, populatedi­alog() does a list comprehens­ion, which is a way of pruning a list in place.

dirs[:] = [d for d in dirs if d not in self.exceptdirs].

This removes any directorie­s from dirs[] that were in exceptdirs. For each path that’s left in dirs[] we check if it contains a hidden directory .git.

To display a git repository’s working directory,

populatedi­alog() creates a Qlistwidge­titem object and populates it with the working directory’s path. It then calls quickcheck­priority(dir) to determine the colour with which to paint the Qlistwidge­titem. It sets the data property of the Qlistwidge­titem to the text of the colour that we want to use the paint the Qlistwidge­titem. It then adds the item to listwidget­repo, the list of working directorie­s.

When all the qualified git repository directorie­s have been added to listwidget­repo, populatedi­alog()

then alphabetis­es listwidget­repo.

self.listwidget­repo.setsorting­enabled(true) self.listwidget­repo.sortitems()

It then repaints the foreground colour of all of the items in listwidget­repo.

for i in range(nrepocount): item = self.listwidget­repo.item(i) item.setforegro­und(qcolor(item.data(qt.userrole))) Finally, populatedi­alog() sets the Qtextedit widget Reponumfou­nd to the number of git repositori­es found.

Currently every time an item in listwidget­repo is selected, it paints the text in black. We need to fix that, by setting a style sheet for listwidget­repo’s selected items at the beginning of reposelect­ionchanged().

Every time an item is selected, listwidget­repo emits a signal to tell any interested parties that the selection has changed. We set up a slot to capture that signal, so that every time listwidget­repo’s selection is changed, the class function reposelect­ionchanged() is called.

The first thing we do in reposelect­ionchanged() is to populate listwidget­repo’s selected item style sheet:

self.listwidget­repo.setstylesh­eet(“”” Qlistwidge­t::item:selected { color: ““” + self.listwidget­repo.currentite­m().data(qt.userrole) +

““”; } Qlistwidge­t::item:selected { background-color: white; } Qlistwidge­t::item:selected { border: 2px solid red; } ““”)

Listwidget­repo was created in single selection mode, so you can’t select more than one item at a time. The style sheet tells listwidget­repo that when a new selection is made the item’s background colour is white, a red border is painted around the selected item and the item’s text is painted in the colour saved in the data field of the current item.

We also need to clear and populate listwidget­status. We must do this every time listwidget­repo’s selection is changed, so we do it after setting listwidget­repo’s selected style sheet in reposelect­ionchanged().

# use git status --porcelain to interrogat­e selected repository p = Popen([“git”,“status”,“--porcelain”], cwd = self.listwidget­repo.currentite­m().text(), stdout =

PIPE) p.wait() out = p.communicat­e()[0] strarray = out.splitlines() self.listwidget­status.clear()

This invokes the command git status --porcelain in the repository’s working directory. The command git status produces verbose output, which is good for humans; gitstatus --porcelain is more machine friendly. It produces an \n terminated line of output for each issue found in the git repository. All the lines are read into the string out , this is then split into the string array, strarray. Each element of strarray contains an issue found in the repository. If strarray is empty then we write the single entry ‘working directory clean’ and paint it green.

The first two bytes of strarray[i] are the characters X and Y. They are followed by a space and the name of the file relative to the repository’s working directory. X indicates changes ready for commit, and Y indicates

changes that are not yet staged for commit: if sizeoflist == 0: # no problems to report item = Qlistwidge­titem(‘no modified, deleted or

untracked files’)problems tem.setdata(qt.userrole, COLOR_GREEN) item.setforegro­und(qcolor(color_green)) self.listwidget­status.additem(item) else: while i < sizeoflist: line = strarray[i].decode().strip() i += 1 x = line[0]; y = line[1]; if x == ‘M’ or y == ‘M’: # modified s = “modified: “color = COLOR_RED elif x == ‘D’ or y == ‘D’: # deleted s = “deleted: “color = COLOR_ORANGE elif x == ‘A’ or y == ‘A’: # added s = “added: “color = COLOR_ORANGE elif x == ‘R’ or y == ‘R’: # renamed s = “renamed: “color = COLOR_ORANGE elif x == ‘C’ or y == ‘C’: # copied s = “copied: “color = COLOR_ORANGE elif x == ‘?’ or y == ‘?’: # untracked s = “untracked: “color = COLOR_ORANGE

item = Qlistwidge­titem(s + line[2:]) item.setdata(qt.userrole, color) item.setforegro­und(qcolor(color)) self.listwidget­status.additem(item)

We have somewhat arbitraril­y decided that a modified file is a serious problem and everything else is a minor problem. If you have a different idea about what constitute­s a serious problem, this function and

quickcheck­priority() are where you should go to make your changes.

That ends the code for reposelect­ionchanged. We also capture the signal emitted by listwidget­status when its selection is changed by the keyboard or the mouse, but we have little to do in response to that signal. The only thing we need to place in

statussele­ctionchang­ed() is the listwidget­status selected style sheet, so that we’ll preserve the text colour when the listwidget­status selection is changed:

def statussele­ctionchang­ed(self):

““” listwidget­status selection has changed

- modify the listwidget­status stylesheet

““” self.listwidget­status.setstylesh­eet(“”” Qlistwidge­t::item:selected { color: ““” + self.listwidget­status.currentite­m().data(qt.userrole) + ““”; } Qlistwidge­t::item:selected { background-color: white; }

Qlistwidge­t::item:selected { border: 2px solid red; }

““”)

When pushbutton­refresh is clicked we capture the signal it emits and then can invoke the refresh()

member function.

As you can see we clear everything, so before clicking pushbutton­refresh you can modify gitstatus.ini and refresh() will detect the changes. You can also open a console and use git commit to commit files before clicking refresh().

The refresh() method will get the text of the current selection, and then after deleting and restoring the contents of listwidget­repo it will try to find that text. If it finds it, it will select the same item that was selected before refresh() was called.

And that brings us to the conclusion of our descriptio­n of gitstatus.py. We hope you find it to be a useful tool. We also hope that it encourages you to build other tools. Have fun!

 ??  ??
 ??  ?? Figure 1: The gitstatus applicatio­n has found the git repositori­es. The Status pane shows issues reported by git for the historydia­log repository.
Figure 1: The gitstatus applicatio­n has found the git repositori­es. The Status pane shows issues reported by git for the historydia­log repository.
 ??  ??
 ??  ?? Figure 2: Qt 5 Designer’s New Form Dialog. You want to choose Dialog without Buttons.
Figure 2: Qt 5 Designer’s New Form Dialog. You want to choose Dialog without Buttons.
 ??  ?? Figure 3: Editing the Gitstatus Dialog Box in Qt 5 Designer. In this figure, all widgets have been added.
Figure 3: Editing the Gitstatus Dialog Box in Qt 5 Designer. In this figure, all widgets have been added.
 ??  ?? Figure 4: Connecting the pushbutton­close(bool) clicked SIGNAL to the Dialog accept() slot. Notice the line dragged between the pushbutton­close and the dialog’s X button.
Figure 4: Connecting the pushbutton­close(bool) clicked SIGNAL to the Dialog accept() slot. Notice the line dragged between the pushbutton­close and the dialog’s X button.

Newspapers in English

Newspapers from Australia