]> Sergey Matveev's repositories - nnn.git/commitdiff
Add user patch framework (#1037)
authorluukvbaal <31730729+luukvbaal@users.noreply.github.com>
Wed, 2 Jun 2021 04:27:06 +0000 (06:27 +0200)
committerGitHub <noreply@github.com>
Wed, 2 Jun 2021 04:27:06 +0000 (09:57 +0530)
* Add user patch framework

* Add git status patch

* Add namefirst-gitstatus compatibility

* Add patch targets

* Fix gitstatus colors and patch order

Makefile
misc/haiku/Makefile
misc/patches/README.md [new file with mode: 0644]
misc/patches/gitstatus/mainline.diff [new file with mode: 0644]
misc/patches/gitstatus/namefirst.diff [new file with mode: 0644]
misc/patches/namefirst/mainline.diff [new file with mode: 0644]

index 994fc09af79557e080e1ba7923b07d87a6573ac8..7e2d7be04a6439393b0de962f90d6e7c4446449a 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -27,6 +27,14 @@ O_NOSSN := 0  # enable session support
 O_NOUG := 0  # disable user, group name in status bar
 O_NOX11 := 0  # disable X11 integration
 
+# User patches
+O_GITSTATUS := 0 # add git status to detail view
+O_NAMEFIRST := 0 # print file name first, add uid and guid to detail view
+
+ifeq ($(strip $(O_GITSTATUS)),1)
+       LDLIBS += -lgit2
+endif
+
 # convert targets to flags for backwards compatibility
 ifneq ($(filter debug,$(MAKECMDGOALS)),)
        O_DEBUG := 1
@@ -141,10 +149,15 @@ DESKTOPFILE = misc/desktop/nnn.desktop
 LOGOSVG = misc/logo/logo.svg
 LOGO64X64 = misc/logo/logo-64x64.png
 
+GITSTATUS = misc/patches/gitstatus
+NAMEFIRST = misc/patches/namefirst
+
 all: $(BIN)
 
 $(BIN): $(SRC) $(HEADERS)
+       @$(MAKE) --silent prepatch
        $(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LDLIBS)
+       @$(MAKE) --silent postpatch
 
 # targets for backwards compatibility
 debug: $(BIN)
@@ -232,6 +245,26 @@ upload-local: sign static
 clean:
        $(RM) -f $(BIN) nnn-$(VERSION).tar.gz *.sig $(BIN)-static $(BIN)-static-$(VERSION).x86_64.tar.gz $(BIN)-icons-static $(BIN)-icons-static-$(VERSION).x86_64.tar.gz $(BIN)-nerd-static $(BIN)-nerd-static-$(VERSION).x86_64.tar.gz
 
+prepatch:
+ifeq ($(strip $(O_NAMEFIRST)),1)
+       patch --forward --strip=1 --input=$(NAMEFIRST)/mainline.diff
+ifeq ($(strip $(O_GITSTATUS)),1)
+       patch --forward --strip=1 --input=$(GITSTATUS)/namefirst.diff
+endif
+else ifeq ($(strip $(O_GITSTATUS)),1)
+       patch --forward --strip=1 --input=$(GITSTATUS)/mainline.diff
+endif
+
+postpatch:
+ifeq ($(strip $(O_NAMEFIRST)),1)
+ifeq ($(strip $(O_GITSTATUS)),1)
+       patch --reverse --strip=1 --input=$(GITSTATUS)/namefirst.diff
+endif
+       patch --reverse --strip=1 --input=$(NAMEFIRST)/mainline.diff
+else ifeq ($(strip $(O_GITSTATUS)),1)
+       patch --reverse --strip=1 --input=$(GITSTATUS)/mainline.diff
+endif
+
 skip: ;
 
 .PHONY: all install uninstall strip static dist sign upload-local clean install-desktop uninstall-desktop
index faf40ec6588283a35015f9447407d935543f57e5..9a721ac8a44f410cc0dc3ee6bc29ab66d617eb8a 100644 (file)
@@ -25,6 +25,14 @@ O_NOSSN := 0  # enable session support
 O_NOUG := 0  # disable user, group name in status bar
 O_NOX11 := 0  # disable X11 integration
 
+# User patches
+O_GITSTATUS := 0 # add git status to detail view
+O_NAMEFIRST := 0 # print file name first, add uid and guid to detail view
+
+ifeq ($(strip $(O_GITSTATUS)),1)
+       LDLIBS += -lgit2
+endif
+
 # convert targets to flags for backwards compatibility
 ifneq ($(filter debug,$(MAKECMDGOALS)),)
        O_DEBUG := 1
@@ -143,6 +151,9 @@ HEADERS = src/nnn.h
 BIN = nnn
 OBJS := nnn.o $(OBJS_HAIKU)
 
+GITSTATUS = misc/patches/gitstatus
+NAMEFIRST = misc/patches/namefirst
+
 all: $(BIN)
 
 ifeq ($(shell uname -s), Haiku)
@@ -154,7 +165,9 @@ nnn.o: $(SRC) $(HEADERS)
        $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $<
 
 $(BIN): $(OBJS)
+       @$(MAKE) --silent prepatch
        $(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS)
+       @$(MAKE) --silent postpatch
 
 # targets for backwards compatibility
 debug: $(BIN)
@@ -207,6 +220,26 @@ upload-local: sign static
 clean:
        $(RM) -f $(BIN) nnn-$(VERSION).tar.gz *.sig $(BIN)-static $(BIN)-static-$(VERSION).x86_64.tar.gz
 
+prepatch:
+ifeq ($(strip $(O_NAMEFIRST)),1)
+       patch --forward --strip=1 --input=$(NAMEFIRST)/mainline.diff
+ifeq ($(strip $(O_GITSTATUS)),1)
+       patch --forward --strip=1 --input=$(GITSTATUS)/namefirst.diff
+endif
+else ifeq ($(strip $(O_GITSTATUS)),1)
+       patch --forward --strip=1 --input=$(GITSTATUS)/mainline.diff
+endif
+
+postpatch:
+ifeq ($(strip $(O_NAMEFIRST)),1)
+ifeq ($(strip $(O_GITSTATUS)),1)
+       patch --reverse --strip=1 --input=$(GITSTATUS)/namefirst.diff
+endif
+       patch --reverse --strip=1 --input=$(NAMEFIRST)/mainline.diff
+else ifeq ($(strip $(O_GITSTATUS)),1)
+       patch --reverse --strip=1 --input=$(GITSTATUS)/mainline.diff
+endif
+
 skip: ;
 
 .PHONY: all install uninstall strip static dist sign upload-local clean
diff --git a/misc/patches/README.md b/misc/patches/README.md
new file mode 100644 (file)
index 0000000..44f8709
--- /dev/null
@@ -0,0 +1,14 @@
+<h1 align="center">User Patch Framework</h1>
+
+This directory contains user submitted patches that were rejected from mainline as they tend to be more subjective in nature. The patches will be adapted on each release when necessary (v4.1 onwards). Each patch can be applied through its respective make variable during compilation. In case inter-patch merge conflicts occur, a compatability patch is provided and will automatically be applied.
+
+## List of patches
+| Patch (a-z) | Description | Make variable |
+| --- | --- | --- |
+| gitstatus | Add git status column to the detail view. Requires [libgit2](https://github.com/libgit2/libgit2). | O_GISTATUS |
+| namefirst | Print filenames first in the detail view. Print user/group columns when a directory contains different users/groups. | O_NAMEFIRST |
+
+To apply the patches, use the corresponding make variables, e.g.:
+
+    make O_NAMEFIRST=1
+
diff --git a/misc/patches/gitstatus/mainline.diff b/misc/patches/gitstatus/mainline.diff
new file mode 100644 (file)
index 0000000..1179e8c
--- /dev/null
@@ -0,0 +1,233 @@
+# Description: Add git status column to detail mode.
+#
+# Dependencies: libgit2
+#
+# Authors: @crides, Luuk van Baal
+diff --git a/src/nnn.c b/src/nnn.c
+index 562622f..96d946a 100644
+--- a/src/nnn.c
++++ b/src/nnn.c
+@@ -127,6 +127,8 @@
+ #include "qsort.h"
+ #endif
+
++#include <git2.h>
++
+ /* Macro definitions */
+ #define VERSION "4.0"
+ #define GENERAL_INFO "BSD 2-Clause\nhttps://github.com/jarun/nnn"
+@@ -245,6 +247,18 @@ typedef unsigned char uchar_t;
+ typedef unsigned short ushort_t;
+ typedef unsigned long long ulong_t;
+
++typedef enum {
++      GIT_COLUMN_STATUS_NONE = 0,
++      GIT_COLUMN_STATUS_UNMOD,
++      GIT_COLUMN_STATUS_NEW,
++      GIT_COLUMN_STATUS_MODIFIED,
++      GIT_COLUMN_STATUS_DELETED,
++      GIT_COLUMN_STATUS_RENAMED,
++      GIT_COLUMN_STATUS_TYPE_CHANGE,
++      GIT_COLUMN_STATUS_IGNORED,
++      GIT_COLUMN_STATUS_CONFLICTED,
++} git_column_status_t;
++
+ /* STRUCTURES */
+
+ /* Directory entry */
+@@ -258,6 +272,8 @@ typedef struct entry {
+               ulong_t blocks : 40; /* 5 bytes (enough for 512 TiB in 512B blocks allocated) */
+               ulong_t nlen : 16; /* 2 bytes (length of file name) */
+               ulong_t flags : 8; /* 1 byte (flags specific to the file) */
++              git_column_status_t status_indexed;
++              git_column_status_t status_staged;
+       };
+ #ifndef NOUG
+       uid_t uid; /* 4 bytes */
+@@ -362,7 +378,18 @@ typedef struct {
+ } session_header_t;
+ #endif
+
++typedef struct {
++      char *path;
++      git_status_t status;
++} simple_git_status_t;
++
++typedef struct {
++      simple_git_status_t *statuses;
++      size_t len;
++} simple_git_statuses_t;
++
+ /* GLOBALS */
++simple_git_statuses_t git_statuses;
+
+ /* Configuration, contexts */
+ static settings cfg = {
+@@ -809,6 +836,92 @@ static void notify_fifo(bool force);
+
+ /* Functions */
+
++static git_column_status_t git_get_indexed_status(const uint32_t status) {
++      if (status & GIT_STATUS_INDEX_NEW)         return GIT_COLUMN_STATUS_NEW;
++      if (status & GIT_STATUS_INDEX_MODIFIED)    return GIT_COLUMN_STATUS_MODIFIED;
++      if (status & GIT_STATUS_INDEX_DELETED)     return GIT_COLUMN_STATUS_DELETED;
++      if (status & GIT_STATUS_INDEX_RENAMED)     return GIT_COLUMN_STATUS_RENAMED;
++      if (status & GIT_STATUS_INDEX_TYPECHANGE)  return GIT_COLUMN_STATUS_TYPE_CHANGE;
++      return GIT_COLUMN_STATUS_UNMOD;
++}
++
++static git_column_status_t git_get_staged_status(const uint32_t status) {
++      if (status & GIT_STATUS_WT_NEW)         return GIT_COLUMN_STATUS_NEW;
++      if (status & GIT_STATUS_WT_MODIFIED)    return GIT_COLUMN_STATUS_MODIFIED;
++      if (status & GIT_STATUS_WT_DELETED)     return GIT_COLUMN_STATUS_DELETED;
++      if (status & GIT_STATUS_WT_RENAMED)     return GIT_COLUMN_STATUS_RENAMED;
++      if (status & GIT_STATUS_WT_TYPECHANGE)  return GIT_COLUMN_STATUS_TYPE_CHANGE;
++      if (status & GIT_STATUS_IGNORED)        return GIT_COLUMN_STATUS_IGNORED;
++      if (status & GIT_STATUS_CONFLICTED)     return GIT_COLUMN_STATUS_CONFLICTED;
++      return GIT_COLUMN_STATUS_UNMOD;
++}
++
++static void print_gitstatus(git_column_status_t status) {
++      switch (status) {
++              case GIT_COLUMN_STATUS_NONE:         break;
++              case GIT_COLUMN_STATUS_UNMOD:        addch('-' | 0); break;
++              case GIT_COLUMN_STATUS_NEW:          addch('N' | (g_state.oldcolor ? COLOR_PAIR(1) : COLOR_PAIR(C_EXE))); break;
++              case GIT_COLUMN_STATUS_MODIFIED:     addch('M' | (g_state.oldcolor ? COLOR_PAIR(1) : COLOR_PAIR(4))); break;
++              case GIT_COLUMN_STATUS_DELETED:      addch('D' | (g_state.oldcolor ? COLOR_PAIR(1) : COLOR_PAIR(C_UND))); break;
++              case GIT_COLUMN_STATUS_RENAMED:      addch('R' | (g_state.oldcolor ? COLOR_PAIR(1) : COLOR_PAIR(C_CHR))); break;
++              case GIT_COLUMN_STATUS_TYPE_CHANGE:  addch('T' | (g_state.oldcolor ? COLOR_PAIR(1) : COLOR_PAIR(C_HRD))); break;
++              case GIT_COLUMN_STATUS_IGNORED:      addch('I' | 0); break;
++              case GIT_COLUMN_STATUS_CONFLICTED:   addch('U' | (g_state.oldcolor ? COLOR_PAIR(1) : COLOR_PAIR(C_UND))); break;
++      }
++}
++
++static bool starts_with(const char *const s, const char *const start) {
++      const size_t start_len = strlen(start);
++
++      return (start_len <= strlen(s)) && (0 == strncmp(s, start, start_len));
++}
++
++static size_t mkpath(const char *dir, const char *name, char *out);
++static simple_git_statuses_t statuses_from_path(const char *path) {
++      simple_git_statuses_t statuses = { .statuses = NULL, .len = 0 };
++      git_buf ret = { .ptr = 0, .asize = 0, .size = 0 };
++      git_repository *repo;
++      git_repository_discover(&ret, path, false, NULL);
++      git_repository_open(&repo, ret.ptr);
++      git_buf_dispose(&ret);
++
++      if (repo) {
++              git_status_list *status_list = NULL;
++              git_status_list_new(&status_list, repo, NULL);
++              statuses.len = git_status_list_entrycount(status_list);
++              statuses.statuses = malloc(statuses.len * sizeof(simple_git_status_t));
++              const char *workdir = git_repository_workdir(repo);
++
++              for (size_t i = 0; i < statuses.len; i ++) {
++                      const git_status_entry *status_ent = git_status_byindex(status_list, i);
++                      const char *entry_path = status_ent->head_to_index ? status_ent->head_to_index->old_file.path
++                                      : status_ent->index_to_workdir->old_file.path;
++                      char *joined = malloc(PATH_MAX * sizeof(char));
++                      mkpath(workdir, entry_path, joined);
++                      char *canon_path = realpath(joined, NULL);
++
++                      if (canon_path) {
++                              statuses.statuses[i].path = canon_path;
++                              free(joined);
++                      } else
++                              statuses.statuses[i].path = joined;
++
++                      statuses.statuses[i].status = status_ent->status;
++              }
++
++              git_status_list_free(status_list);
++              git_repository_free(repo);
++      }
++      return statuses;
++}
++
++static void statuses_free(simple_git_statuses_t statuses) {
++      for (size_t i = 0; i < statuses.len; i ++)
++              free(statuses.statuses[i].path);
++
++      free(statuses.statuses);
++}
++
+ static void sigint_handler(int UNUSED(sig))
+ {
+       g_state.interrupt = 1;
+@@ -3794,6 +3907,12 @@ static void printent(const struct entry *ent, uint_t namecols, bool sel)
+
+               if (attrs)
+                       attroff(attrs);
++
++              if (ent->status_indexed != GIT_COLUMN_STATUS_NONE)
++                      addch(' ');
++
++              print_gitstatus(ent->status_indexed);
++              print_gitstatus(ent->status_staged);
+       }
+
+       attrs = 0;
+@@ -3809,6 +3928,7 @@ static void printent(const struct entry *ent, uint_t namecols, bool sel)
+                       color_pair = C_MIS;
+               if (color_pair && fcolors[color_pair])
+                       attrs |= COLOR_PAIR(color_pair);
++
+ #ifdef ICONS_ENABLED
+               print_icon(ent, attrs);
+ #endif
+@@ -5088,6 +5208,9 @@ static int dentfill(char *path, struct entry **ppdents)
+       struct stat sb_path, sb;
+       DIR *dirp = opendir(path);
+
++      statuses_free(git_statuses);
++      git_statuses = statuses_from_path(path);
++
+       ndents = 0;
+
+       DPRINTF_S(__func__);
+@@ -5277,6 +5400,26 @@ static int dentfill(char *path, struct entry **ppdents)
+               dentp->gid = sb.st_gid;
+ #endif
+
++              if (git_statuses.len) {
++                      uint32_t merged_status = 0;
++                      char joined[PATH_MAX];
++                      mkpath(path, dentp->name, joined);
++                      char *real = realpath(joined, NULL);
++                      char *canon_path = real ? real : joined;
++
++                      for (size_t i = 0; i < git_statuses.len; i ++)
++                              if (((dentp->mode & S_IFMT) == S_IFDIR) ? starts_with(git_statuses.statuses[i].path, canon_path) :
++                                              !xstrcmp(git_statuses.statuses[i].path, canon_path) || starts_with(canon_path, git_statuses.statuses[i].path))
++                                      merged_status |= git_statuses.statuses[i].status;
++
++                      dentp->status_indexed = git_get_indexed_status(merged_status);
++                      dentp->status_staged = git_get_staged_status(merged_status);
++                      free(real);
++              } else {
++                      dentp->status_indexed = GIT_COLUMN_STATUS_NONE;
++                      dentp->status_staged = GIT_COLUMN_STATUS_NONE;
++              }
++
+               dentp->flags = S_ISDIR(sb.st_mode) ? 0 : ((sb.st_nlink > 1) ? HARD_LINK : 0);
+               if (entflags) {
+                       dentp->flags |= entflags;
+@@ -7712,6 +7855,8 @@ static void cleanup(void)
+               fflush(stdout);
+       }
+ #endif
++      statuses_free(git_statuses);
++      git_libgit2_shutdown();
+       free(selpath);
+       free(plgpath);
+       free(cfgpath);
+@@ -7907,6 +8052,7 @@ int main(int argc, char *argv[])
+               return EXIT_FAILURE;
+
+       atexit(cleanup);
++      git_libgit2_init();
+
+       /* Check if we are in path list mode */
+       if (!isatty(STDIN_FILENO)) {
diff --git a/misc/patches/gitstatus/namefirst.diff b/misc/patches/gitstatus/namefirst.diff
new file mode 100644 (file)
index 0000000..77d4de9
--- /dev/null
@@ -0,0 +1,226 @@
+# Description: Add git status column to detail mode.
+#              Compatibility patch for the namefirst patch.
+#
+# Dependencies: libgit2
+#
+# Authors: @crides, Luuk van Baaldiff --git a/src/nnn.c b/src/nnn.c
+index d66e5da..5770f1a 100644
+--- a/src/nnn.c
++++ b/src/nnn.c
+@@ -127,6 +127,8 @@
+ #include "qsort.h"
+ #endif
+
++#include <git2.h>
++
+ /* Macro definitions */
+ #define VERSION "4.0"
+ #define GENERAL_INFO "BSD 2-Clause\nhttps://github.com/jarun/nnn"
+@@ -245,6 +247,18 @@ typedef unsigned char uchar_t;
+ typedef unsigned short ushort_t;
+ typedef unsigned long long ulong_t;
+
++typedef enum {
++      GIT_COLUMN_STATUS_NONE = 0,
++      GIT_COLUMN_STATUS_UNMOD,
++      GIT_COLUMN_STATUS_NEW,
++      GIT_COLUMN_STATUS_MODIFIED,
++      GIT_COLUMN_STATUS_DELETED,
++      GIT_COLUMN_STATUS_RENAMED,
++      GIT_COLUMN_STATUS_TYPE_CHANGE,
++      GIT_COLUMN_STATUS_IGNORED,
++      GIT_COLUMN_STATUS_CONFLICTED,
++} git_column_status_t;
++
+ /* STRUCTURES */
+
+ /* Directory entry */
+@@ -258,6 +272,8 @@ typedef struct entry {
+               ulong_t blocks : 40; /* 5 bytes (enough for 512 TiB in 512B blocks allocated) */
+               ulong_t nlen : 16; /* 2 bytes (length of file name) */
+               ulong_t flags : 8; /* 1 byte (flags specific to the file) */
++              git_column_status_t status_indexed;
++              git_column_status_t status_staged;
+       };
+ #ifndef NOUG
+       uid_t uid; /* 4 bytes */
+@@ -366,7 +382,18 @@ static struct {
+       ushort_t maxnameln, maxsizeln, maxuidln, maxgidln, maxentln, uidln, gidln, printguid;
+ } dtls;
+
++typedef struct {
++      char *path;
++      git_status_t status;
++} simple_git_status_t;
++
++typedef struct {
++      simple_git_status_t *statuses;
++      size_t len;
++} simple_git_statuses_t;
++
+ /* GLOBALS */
++simple_git_statuses_t git_statuses;
+
+ /* Configuration, contexts */
+ static settings cfg = {
+@@ -813,6 +840,92 @@ static void notify_fifo(bool force);
+
+ /* Functions */
+
++static git_column_status_t git_get_indexed_status(const uint32_t status) {
++      if (status & GIT_STATUS_INDEX_NEW)         return GIT_COLUMN_STATUS_NEW;
++      if (status & GIT_STATUS_INDEX_MODIFIED)    return GIT_COLUMN_STATUS_MODIFIED;
++      if (status & GIT_STATUS_INDEX_DELETED)     return GIT_COLUMN_STATUS_DELETED;
++      if (status & GIT_STATUS_INDEX_RENAMED)     return GIT_COLUMN_STATUS_RENAMED;
++      if (status & GIT_STATUS_INDEX_TYPECHANGE)  return GIT_COLUMN_STATUS_TYPE_CHANGE;
++      return GIT_COLUMN_STATUS_UNMOD;
++}
++
++static git_column_status_t git_get_staged_status(const uint32_t status) {
++      if (status & GIT_STATUS_WT_NEW)         return GIT_COLUMN_STATUS_NEW;
++      if (status & GIT_STATUS_WT_MODIFIED)    return GIT_COLUMN_STATUS_MODIFIED;
++      if (status & GIT_STATUS_WT_DELETED)     return GIT_COLUMN_STATUS_DELETED;
++      if (status & GIT_STATUS_WT_RENAMED)     return GIT_COLUMN_STATUS_RENAMED;
++      if (status & GIT_STATUS_WT_TYPECHANGE)  return GIT_COLUMN_STATUS_TYPE_CHANGE;
++      if (status & GIT_STATUS_IGNORED)        return GIT_COLUMN_STATUS_IGNORED;
++      if (status & GIT_STATUS_CONFLICTED)     return GIT_COLUMN_STATUS_CONFLICTED;
++      return GIT_COLUMN_STATUS_UNMOD;
++}
++
++static void print_gitstatus(git_column_status_t status) {
++      switch (status) {
++              case GIT_COLUMN_STATUS_NONE:         break;
++              case GIT_COLUMN_STATUS_UNMOD:        addch('-' | 0); break;
++              case GIT_COLUMN_STATUS_NEW:          addch('N' | (g_state.oldcolor ? COLOR_PAIR(1) : COLOR_PAIR(C_EXE))); break;
++              case GIT_COLUMN_STATUS_MODIFIED:     addch('M' | (g_state.oldcolor ? COLOR_PAIR(1) : COLOR_PAIR(4))); break;
++              case GIT_COLUMN_STATUS_DELETED:      addch('D' | (g_state.oldcolor ? COLOR_PAIR(1) : COLOR_PAIR(C_UND))); break;
++              case GIT_COLUMN_STATUS_RENAMED:      addch('R' | (g_state.oldcolor ? COLOR_PAIR(1) : COLOR_PAIR(C_CHR))); break;
++              case GIT_COLUMN_STATUS_TYPE_CHANGE:  addch('T' | (g_state.oldcolor ? COLOR_PAIR(1) : COLOR_PAIR(C_HRD))); break;
++              case GIT_COLUMN_STATUS_IGNORED:      addch('I' | 0); break;
++              case GIT_COLUMN_STATUS_CONFLICTED:   addch('U' | (g_state.oldcolor ? COLOR_PAIR(1) : COLOR_PAIR(C_UND))); break;
++      }
++}
++
++static bool starts_with(const char *const s, const char *const start) {
++      const size_t start_len = strlen(start);
++
++      return (start_len <= strlen(s)) && (0 == strncmp(s, start, start_len));
++}
++
++static size_t mkpath(const char *dir, const char *name, char *out);
++static simple_git_statuses_t statuses_from_path(const char *path) {
++      simple_git_statuses_t statuses = { .statuses = NULL, .len = 0 };
++      git_buf ret = { .ptr = 0, .asize = 0, .size = 0 };
++      git_repository *repo;
++      git_repository_discover(&ret, path, false, NULL);
++      git_repository_open(&repo, ret.ptr);
++      git_buf_dispose(&ret);
++
++      if (repo) {
++              git_status_list *status_list = NULL;
++              git_status_list_new(&status_list, repo, NULL);
++              statuses.len = git_status_list_entrycount(status_list);
++              statuses.statuses = malloc(statuses.len * sizeof(simple_git_status_t));
++              const char *workdir = git_repository_workdir(repo);
++
++              for (size_t i = 0; i < statuses.len; i ++) {
++                      const git_status_entry *status_ent = git_status_byindex(status_list, i);
++                      const char *entry_path = status_ent->head_to_index ? status_ent->head_to_index->old_file.path
++                                      : status_ent->index_to_workdir->old_file.path;
++                      char *joined = malloc(PATH_MAX * sizeof(char));
++                      mkpath(workdir, entry_path, joined);
++                      char *canon_path = realpath(joined, NULL);
++
++                      if (canon_path) {
++                              statuses.statuses[i].path = canon_path;
++                              free(joined);
++                      } else
++                              statuses.statuses[i].path = joined;
++
++                      statuses.statuses[i].status = status_ent->status;
++              }
++
++              git_status_list_free(status_list);
++              git_repository_free(repo);
++      }
++      return statuses;
++}
++
++static void statuses_free(simple_git_statuses_t statuses) {
++      for (size_t i = 0; i < statuses.len; i ++)
++              free(statuses.statuses[i].path);
++
++      free(statuses.statuses);
++}
++
+ static void sigint_handler(int UNUSED(sig))
+ {
+       g_state.interrupt = 1;
+@@ -3783,6 +3896,13 @@ static void printent(const struct entry *ent, uint_t namecols, bool sel)
+
+       addch((ent->flags & FILE_SELECTED) ? '+' | A_REVERSE | A_BOLD : ' ');
+
++      if (cfg.showdetail) {
++              print_gitstatus(ent->status_indexed);
++              print_gitstatus(ent->status_staged);
++              if (ent->status_indexed != GIT_COLUMN_STATUS_NONE)
++                      addch(' ');
++      }
++
+       if (g_state.oldcolor)
+               resetdircolor(ent->flags);
+       else {
+@@ -5094,6 +5214,9 @@ static int dentfill(char *path, struct entry **ppdents)
+       struct stat sb_path, sb;
+       DIR *dirp = opendir(path);
+
++      statuses_free(git_statuses);
++      git_statuses = statuses_from_path(path);
++
+       ndents = 0;
+
+       DPRINTF_S(__func__);
+@@ -5283,6 +5406,26 @@ static int dentfill(char *path, struct entry **ppdents)
+               dentp->gid = sb.st_gid;
+ #endif
+
++              if (git_statuses.len) {
++                      uint32_t merged_status = 0;
++                      char joined[PATH_MAX];
++                      mkpath(path, dentp->name, joined);
++                      char *real = realpath(joined, NULL);
++                      char *canon_path = real ? real : joined;
++
++                      for (size_t i = 0; i < git_statuses.len; i ++)
++                              if (((dentp->mode & S_IFMT) == S_IFDIR) ? starts_with(git_statuses.statuses[i].path, canon_path) :
++                                              !xstrcmp(git_statuses.statuses[i].path, canon_path) || starts_with(canon_path, git_statuses.statuses[i].path))
++                                      merged_status |= git_statuses.statuses[i].status;
++
++                      dentp->status_indexed = git_get_indexed_status(merged_status);
++                      dentp->status_staged = git_get_staged_status(merged_status);
++                      free(real);
++              } else {
++                      dentp->status_indexed = GIT_COLUMN_STATUS_NONE;
++                      dentp->status_staged = GIT_COLUMN_STATUS_NONE;
++              }
++
+               dentp->flags = S_ISDIR(sb.st_mode) ? 0 : ((sb.st_nlink > 1) ? HARD_LINK : 0);
+               if (entflags) {
+                       dentp->flags |= entflags;
+@@ -7714,6 +7857,8 @@ static void cleanup(void)
+               fflush(stdout);
+       }
+ #endif
++      statuses_free(git_statuses);
++      git_libgit2_shutdown();
+       free(selpath);
+       free(plgpath);
+       free(cfgpath);
+@@ -7909,6 +8054,7 @@ int main(int argc, char *argv[])
+               return EXIT_FAILURE;
+
+       atexit(cleanup);
++      git_libgit2_init();
+
+       /* Check if we are in path list mode */
+       if (!isatty(STDIN_FILENO)) {
diff --git a/misc/patches/namefirst/mainline.diff b/misc/patches/namefirst/mainline.diff
new file mode 100644 (file)
index 0000000..49e614e
--- /dev/null
@@ -0,0 +1,227 @@
+# Description: Prints filenames first in the detail view.  Prints user/group
+#              columns when a directory contains different users/groups.
+#
+# Author: Luuk van Baal
+diff --git a/src/nnn.c b/src/nnn.c
+index 562622fa..d66e5dac 100644
+--- a/src/nnn.c
++++ b/src/nnn.c
+@@ -362,6 +362,10 @@ typedef struct {
+ } session_header_t;
+ #endif
+
++static struct {
++      ushort_t maxnameln, maxsizeln, maxuidln, maxgidln, maxentln, uidln, gidln, printguid;
++} dtls;
++
+ /* GLOBALS */
+
+ /* Configuration, contexts */
+@@ -1038,10 +1042,12 @@ static char *getpwname(uid_t uid)
+       static char *namecache = NULL;
+
+       if (uidcache != uid) {
++              if (dtls.maxuidln && !dtls.printguid) dtls.printguid = 1;
+               struct passwd *pw = getpwuid(uid);
+
+               uidcache = uid;
+               namecache = pw ? pw->pw_name : NULL;
++              dtls.uidln = xstrlen(namecache ? namecache : xitoa(uid));
+       }
+
+       return namecache ? namecache : xitoa(uid);
+@@ -1053,10 +1059,12 @@ static char *getgrname(gid_t gid)
+       static char *grpcache = NULL;
+
+       if (gidcache != gid) {
++              if (dtls.maxgidln && !dtls.printguid) dtls.printguid = 1;
+               struct group *gr = getgrgid(gid);
+
+               gidcache = gid;
+               grpcache = gr ? gr->gr_name : NULL;
++              dtls.gidln = xstrlen(grpcache ? grpcache : xitoa(gid));
+       }
+
+       return grpcache ? grpcache : xitoa(gid);
+@@ -3479,14 +3487,13 @@ static void resetdircolor(int flags)
+  * Max supported str length: NAME_MAX;
+  */
+ #ifdef NOLC
+-static char *unescape(const char *str, uint_t maxcols)
++static size_t unescape(const char *str, uint_t maxcols)
+ {
+       char * const wbuf = g_buf;
+       char *buf = wbuf;
+-
+-      xstrsncpy(wbuf, str, maxcols);
++      size_t len = xstrsncpy(wbuf, str, maxcols);
+ #else
+-static wchar_t *unescape(const char *str, uint_t maxcols)
++static size_t unescape(const char *str, uint_t maxcols)
+ {
+       wchar_t * const wbuf = (wchar_t *)g_buf;
+       wchar_t *buf = wbuf;
+@@ -3510,7 +3517,7 @@ static wchar_t *unescape(const char *str, uint_t maxcols)
+               ++buf;
+       }
+
+-      return wbuf;
++      return len;
+ }
+
+ static off_t get_size(off_t size, off_t *pval, uint_t comp)
+@@ -3771,33 +3778,7 @@ static uchar_t get_color_pair_name_ind(const struct entry *ent, char *pind, int
+ static void printent(const struct entry *ent, uint_t namecols, bool sel)
+ {
+       char ind = '\0';
+-      int attrs;
+-
+-      if (cfg.showdetail) {
+-              int type = ent->mode & S_IFMT;
+-              char perms[6] = {' ', ' ', (char)('0' + ((ent->mode >> 6) & 7)),
+-                              (char)('0' + ((ent->mode >> 3) & 7)),
+-                              (char)('0' + (ent->mode & 7)), '\0'};
+-
+-              addch(' ');
+-              attrs = g_state.oldcolor ? (resetdircolor(ent->flags), A_DIM)
+-                                       : (fcolors[C_MIS] ? COLOR_PAIR(C_MIS) : 0);
+-              if (attrs)
+-                      attron(attrs);
+-
+-              /* Print details */
+-              print_time(&ent->sec);
+-
+-              printw("%s%9s ", perms, (type == S_IFREG || type == S_IFDIR)
+-                      ? coolsize(cfg.blkorder ? (blkcnt_t)ent->blocks << blk_shift : ent->size)
+-                      : (type = (uchar_t)get_detail_ind(ent->mode), (char *)&type));
+-
+-              if (attrs)
+-                      attroff(attrs);
+-      }
+-
+-      attrs = 0;
+-
++      int attrs = 0, namelen;
+       uchar_t color_pair = get_color_pair_name_ind(ent, &ind, &attrs);
+
+       addch((ent->flags & FILE_SELECTED) ? '+' | A_REVERSE | A_BOLD : ' ');
+@@ -3822,15 +3803,40 @@ static void printent(const struct entry *ent, uint_t namecols, bool sel)
+               ++namecols;
+
+ #ifndef NOLC
+-      addwstr(unescape(ent->name, namecols));
++      addwstr((namelen = unescape(ent->name, namecols), (wchar_t *)g_buf));
+ #else
+-      addstr(unescape(ent->name, MIN(namecols, ent->nlen) + 1));
++      addstr((namelen = unescape(ent->name, MIN(namecols, ent->nlen) + 1), (char *)g_buf));
+ #endif
+
+-      if (attrs)
++      if (!sel && attrs)
+               attroff(attrs);
+       if (ind)
+               addch(ind);
++      if (cfg.showdetail) {
++              int type = ent->mode & S_IFMT;
++              char perms[6] = {(char)('0' + ((ent->mode >> 6) & 7)),
++                              (char)('0' + ((ent->mode >> 3) & 7)),
++                              (char)('0' + (ent->mode & 7)), ' ', ' ', '\0'}, *size = NULL;
++
++              if (attrs)
++                      attron(attrs);
++              if (!g_state.oldcolor && (type == S_IFDIR || (type == S_IFLNK && ent->flags & DIR_OR_LINK_TO_DIR)))
++                      attroff(A_BOLD);
++              size_t sizelen = (type == S_IFREG || type == S_IFDIR) ? xstrlen(size = coolsize(cfg.blkorder ? ent->blocks << blk_shift : ent->size)) : 1;
++              printw("%*c%*s%s%s", 1 + MIN(namecols, dtls.maxnameln + (size_t)(ind ? 0 : 1)) - namelen, ' ',
++                              dtls.maxsizeln - sizelen, "", size ? size : (type = (uchar_t)get_detail_ind(ent->mode), (char *)&type), "  ");
++#ifndef NOUG
++              if (g_state.uidgid && dtls.printguid) {
++                      addstr(getpwname(ent->uid));
++                      printw("%*c%s", dtls.maxuidln + 1 - dtls.uidln, ' ', getgrname(ent->gid));
++                      printw("%*c", dtls.maxgidln + 2 - dtls.gidln, ' ');
++              }
++#endif
++              addstr(perms);
++              print_time(&ent->sec);
++      }
++      if (attrs)
++              attroff(attrs);
+ }
+
+ static void savecurctx(settings *curcfg, char *path, char *curname, int nextctx)
+@@ -5814,18 +5820,6 @@ static void statusbar(char *path)
+               tocursor();
+ }
+
+-static inline void markhovered(void)
+-{
+-      if (cfg.showdetail && ndents) { /* Reversed block for hovered entry */
+-              tocursor();
+-#ifdef ICONS_ENABLED
+-              addstr(MD_ARROW_FORWARD);
+-#else
+-              addch(' ' | A_REVERSE);
+-#endif
+-      }
+-}
+-
+ static int adjust_cols(int n)
+ {
+       /* Calculate the number of cols available to print entry name */
+@@ -5833,13 +5827,10 @@ static int adjust_cols(int n)
+       n -= (g_state.oldcolor ? 0 : 1 + xstrlen(ICON_PADDING_LEFT) + xstrlen(ICON_PADDING_RIGHT));
+ #endif
+       if (cfg.showdetail) {
+-              /* Fallback to light mode if less than 35 columns */
+-              if (n < 36)
++              if (n < (dtls.maxentln + 1 - dtls.maxnameln))
+                       cfg.showdetail ^= 1;
+-              else {
+-                      /* 3 more accounted for below */
+-                      n -= 32;
+-              }
++              else
++                      n -= (dtls.maxentln - 2 - dtls.maxnameln);
+       }
+
+       /* 2 columns for preceding space and indicator */
+@@ -5877,8 +5868,6 @@ static void draw_line(char *path, int ncols)
+       if (dir)
+               attroff(COLOR_PAIR(cfg.curctx + 1) | A_BOLD);
+
+-      markhovered();
+-
+       statusbar(path);
+ }
+
+@@ -5970,6 +5959,21 @@ static void redraw(char *path)
+
+       attroff(A_UNDERLINE | COLOR_PAIR(cfg.curctx + 1));
+
++      if (cfg.showdetail) {
++      ushort_t lenbuf = dtls.maxnameln = dtls.maxsizeln = dtls.maxuidln = dtls.maxgidln = dtls.printguid = 0;
++              for (i = curscroll; i < ndents && i < curscroll + onscreen; ++i) {
++                      if ((lenbuf = pdents[i].nlen - 1) > dtls.maxnameln) dtls.maxnameln = lenbuf;
++                      if ((lenbuf = xstrlen(coolsize(cfg.blkorder ? pdents[i].blocks << blk_shift : pdents[i].size))) > dtls.maxsizeln) dtls.maxsizeln = lenbuf;
++#ifndef NOUG
++                      if (g_state.uidgid) {
++                              if ((getpwname(pdents[i].uid), dtls.uidln) > dtls.maxuidln) dtls.maxuidln = dtls.uidln;
++                              if ((getgrname(pdents[i].gid), dtls.gidln) > dtls.maxgidln) dtls.maxgidln = dtls.gidln;
++                      }
++#endif
++              }
++      }
++      dtls.maxentln = dtls.maxnameln + dtls.maxsizeln + dtls.maxuidln + dtls.maxgidln + (g_state.uidgid ? 29 : 26);
++
+       ncols = adjust_cols(ncols);
+
+       /* Go to first entry */
+@@ -6011,8 +6015,6 @@ static void redraw(char *path)
+ #endif
+       }
+
+-      markhovered();
+-
+       statusbar(path);
+ }
+