Received: from mail.netbsd.org (mail.netbsd.org [199.233.217.200]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (Client CN "mail.netbsd.org", Issuer "Postmaster NetBSD.org" (verified OK)) by mollari.NetBSD.org (Postfix) with ESMTPS id 8D7137A13C for ; Mon, 14 Nov 2016 01:08:25 +0000 (UTC) Received: by mail.netbsd.org (Postfix, from userid 605) id 396E2855C2; Mon, 14 Nov 2016 01:08:25 +0000 (UTC) Received: from localhost (localhost [127.0.0.1]) by mail.netbsd.org (Postfix) with ESMTP id BDCF985571 for ; Mon, 14 Nov 2016 01:08:24 +0000 (UTC) X-Virus-Scanned: amavisd-new at netbsd.org Received: from mail.netbsd.org ([IPv6:::1]) by localhost (mail.netbsd.org [IPv6:::1]) (amavisd-new, port 10025) with ESMTP id Fba0aotyq8uI for ; Mon, 14 Nov 2016 01:08:23 +0000 (UTC) Received: from cvs.NetBSD.org (ivanova.netbsd.org [199.233.217.197]) by mail.netbsd.org (Postfix) with ESMTP id 8CC9885592 for ; Mon, 14 Nov 2016 01:08:23 +0000 (UTC) Received: by cvs.NetBSD.org (Postfix, from userid 500) id 8A2A5FBA6; Mon, 14 Nov 2016 01:08:23 +0000 (UTC) Content-Transfer-Encoding: 7bit Content-Type: multipart/mixed; boundary="_----------=_1479085703230680" MIME-Version: 1.0 Date: Mon, 14 Nov 2016 01:08:23 +0000 From: "Roland Illig" Subject: CVS commit: pkgsrc/pkgtools/pkglint To: pkgsrc-changes@NetBSD.org Reply-To: rillig@netbsd.org X-Mailer: log_accum Message-Id: <20161114010823.8A2A5FBA6@cvs.NetBSD.org> Sender: pkgsrc-changes-owner@NetBSD.org List-Id: pkgsrc-changes.NetBSD.org Precedence: bulk This is a multi-part message in MIME format. --_----------=_1479085703230680 Content-Disposition: inline Content-Transfer-Encoding: 8bit Content-Type: text/plain; charset="US-ASCII" Module Name: pkgsrc Committed By: rillig Date: Mon Nov 14 01:08:23 UTC 2016 Modified Files: pkgsrc/pkgtools/pkglint: Makefile pkgsrc/pkgtools/pkglint/files: globaldata.go globalvars.go logging.go main.go mkline.go mkline_test.go package.go pkglint.go plist_test.go util.go util_test.go vardefs.go Log Message: Updated pkglint to 5.4.11. Changes since 5.4.10: * Replaced regular expression with hand-written matching code, since it is 30 times as fast. * Reduced number of syscalls by remembering os.Lstat results and CVS/Entries. * Reduced number of syscalls by querying the current user only once. * Added warning for comparing ${PKGSRC_COMPILER} == "clang", which should rather be ${PKGSRC_COMPILER:Mclang}. * Added variable definitions for NOT_PAX_ASLR_SAFE and NOT_PAX_MPROTECT_SAFE. To generate a diff of this commit: cvs rdiff -u -r1.499 -r1.500 pkgsrc/pkgtools/pkglint/Makefile cvs rdiff -u -r1.14 -r1.15 pkgsrc/pkgtools/pkglint/files/globaldata.go cvs rdiff -u -r1.3 -r1.4 pkgsrc/pkgtools/pkglint/files/globalvars.go cvs rdiff -u -r1.5 -r1.6 pkgsrc/pkgtools/pkglint/files/logging.go \ pkgsrc/pkgtools/pkglint/files/util_test.go cvs rdiff -u -r1.8 -r1.9 pkgsrc/pkgtools/pkglint/files/main.go cvs rdiff -u -r1.16 -r1.17 pkgsrc/pkgtools/pkglint/files/mkline.go cvs rdiff -u -r1.17 -r1.18 pkgsrc/pkgtools/pkglint/files/mkline_test.go \ pkgsrc/pkgtools/pkglint/files/vardefs.go cvs rdiff -u -r1.12 -r1.13 pkgsrc/pkgtools/pkglint/files/package.go cvs rdiff -u -r1.13 -r1.14 pkgsrc/pkgtools/pkglint/files/pkglint.go cvs rdiff -u -r1.9 -r1.10 pkgsrc/pkgtools/pkglint/files/plist_test.go \ pkgsrc/pkgtools/pkglint/files/util.go Please note that diffs are not public domain; they are subject to the copyright notices on the relevant files. --_----------=_1479085703230680 Content-Disposition: inline Content-Length: 16657 Content-Transfer-Encoding: binary Content-Type: text/x-diff; charset=utf-8 Modified files: Index: pkgsrc/pkgtools/pkglint/Makefile diff -u pkgsrc/pkgtools/pkglint/Makefile:1.499 pkgsrc/pkgtools/pkglint/Makefile:1.500 --- pkgsrc/pkgtools/pkglint/Makefile:1.499 Tue Nov 1 21:40:25 2016 +++ pkgsrc/pkgtools/pkglint/Makefile Mon Nov 14 01:08:23 2016 @@ -1,6 +1,6 @@ -# $NetBSD: Makefile,v 1.499 2016/11/01 21:40:25 rillig Exp $ +# $NetBSD: Makefile,v 1.500 2016/11/14 01:08:23 rillig Exp $ -PKGNAME= pkglint-5.4.10 +PKGNAME= pkglint-5.4.11 DISTFILES= # none CATEGORIES= pkgtools Index: pkgsrc/pkgtools/pkglint/files/globaldata.go diff -u pkgsrc/pkgtools/pkglint/files/globaldata.go:1.14 pkgsrc/pkgtools/pkglint/files/globaldata.go:1.15 --- pkgsrc/pkgtools/pkglint/files/globaldata.go:1.14 Tue Sep 6 20:54:21 2016 +++ pkgsrc/pkgtools/pkglint/files/globaldata.go Mon Nov 14 01:08:23 2016 @@ -114,7 +114,7 @@ func (gd *GlobalData) loadTools() { fname := G.globalData.Pkgsrcdir + "/mk/tools/bsd.tools.mk" lines := LoadExistingLines(fname, true) for _, line := range lines { - if m, _, _, includefile := match3(line.Text, reMkInclude); m { + if m, _, _, includefile := MatchMkInclude(line.Text); m { if !contains(includefile, "/") { toolFiles = append(toolFiles, includefile) } Index: pkgsrc/pkgtools/pkglint/files/globalvars.go diff -u pkgsrc/pkgtools/pkglint/files/globalvars.go:1.3 pkgsrc/pkgtools/pkglint/files/globalvars.go:1.4 --- pkgsrc/pkgtools/pkglint/files/globalvars.go:1.3 Sun Jun 5 11:24:32 2016 +++ pkgsrc/pkgtools/pkglint/files/globalvars.go Mon Nov 14 01:08:23 2016 @@ -11,12 +11,15 @@ type GlobalVars struct { Pkg *Package // The package that is currently checked. Mk *MkLines // The Makefile (or fragment) that is currently checked. - Todo []string // The files or directories that still need to be checked. - CurrentDir string // The currently checked directory, relative to the cwd - CurPkgsrcdir string // The pkgsrc directory, relative to currentDir - Wip bool // Is the currently checked directory from pkgsrc-wip? - Infrastructure bool // Is the currently checked item from the pkgsrc infrastructure? - Testing bool // Is pkglint in self-testing mode (only during development)? + Todo []string // The files or directories that still need to be checked. + CurrentDir string // The currently checked directory, relative to the cwd + CurPkgsrcdir string // The pkgsrc directory, relative to currentDir + Wip bool // Is the currently checked directory from pkgsrc-wip? + Infrastructure bool // Is the currently checked item from the pkgsrc infrastructure? + Testing bool // Is pkglint in self-testing mode (only during development)? + CurrentUsername string // For checking against OWNER and MAINTAINER + CvsEntriesDir string // Cached to avoid I/O + CvsEntriesLines []*Line Hash map[string]*Hash // Maps "alg:fname" => hash (inter-package check). UsedLicenses map[string]bool // Maps "license name" => true (inter-package check). @@ -32,10 +35,10 @@ type GlobalVars struct { logErr io.Writer debugOut io.Writer - res map[string]*regexp.Regexp + res map[string]*regexp.Regexp // Compiled regular expressions rematch *Histogram renomatch *Histogram - retime *Histogram + retime *Histogram // Total time taken by matching a regular expression loghisto *Histogram } Index: pkgsrc/pkgtools/pkglint/files/logging.go diff -u pkgsrc/pkgtools/pkglint/files/logging.go:1.5 pkgsrc/pkgtools/pkglint/files/logging.go:1.6 --- pkgsrc/pkgtools/pkglint/files/logging.go:1.5 Sun Jun 5 11:24:32 2016 +++ pkgsrc/pkgtools/pkglint/files/logging.go Mon Nov 14 01:08:23 2016 @@ -105,7 +105,7 @@ func Explain(explanation ...string) { print(fmt.Sprintf("Long explanation line (%d): %s\n", l, s)) } if m, before := match1(s, `(.+)\. [^ ]`); m { - if !matches(before, `\d$`) { + if !matches(before, `\d$|e\.g`) { print(fmt.Sprintf("Short space after period: %s\n", s)) } } Index: pkgsrc/pkgtools/pkglint/files/util_test.go diff -u pkgsrc/pkgtools/pkglint/files/util_test.go:1.5 pkgsrc/pkgtools/pkglint/files/util_test.go:1.6 --- pkgsrc/pkgtools/pkglint/files/util_test.go:1.5 Sat Jul 9 09:43:48 2016 +++ pkgsrc/pkgtools/pkglint/files/util_test.go Mon Nov 14 01:08:23 2016 @@ -2,6 +2,7 @@ package main import ( check "gopkg.in/check.v1" + "testing" ) func (s *Suite) Test_MkopSubst__middle(c *check.C) { @@ -86,3 +87,60 @@ func (s *Suite) Test_PrefixReplacer_Sinc repl.AdvanceRegexp(`^\w+`) c.Check(repl.Since(mark), equals, "hello") } + +const reMkIncludeBenchmark = `^\.(\s*)(s?include)\s+\"([^\"]+)\"\s*(?:#.*)?$` +const reMkIncludeBenchmarkPositive = `^\.(\s*)(s?include)\s+\"(.+)\"\s*(?:#.*)?$` + +func Benchmark_match3_buildlink3(b *testing.B) { + for i := 0; i < b.N; i++ { + match3(".include \"../../category/package/buildlink3.mk\"", reMkIncludeBenchmark) + } +} + +func Benchmark_match3_bsd_pkg_mk(b *testing.B) { + for i := 0; i < b.N; i++ { + match3(".include \"../../mk/bsd.pkg.mk\"", reMkIncludeBenchmark) + } +} + +func Benchmark_match3_samedir(b *testing.B) { + for i := 0; i < b.N; i++ { + match3(".include \"options.mk\"", reMkIncludeBenchmark) + } +} + +func Benchmark_match3_bsd_pkg_mk_comment(b *testing.B) { + for i := 0; i < b.N; i++ { + match3(".include \"../../mk/bsd.pkg.mk\" # infrastructure ", reMkIncludeBenchmark) + } +} + +func Benchmark_match3_buildlink3_positive(b *testing.B) { + for i := 0; i < b.N; i++ { + match3(".include \"../../category/package/buildlink3.mk\"", reMkIncludeBenchmarkPositive) + } +} + +func Benchmark_match3_bsd_pkg_mk_positive(b *testing.B) { + for i := 0; i < b.N; i++ { + match3(".include \"../../mk/bsd.pkg.mk\"", reMkIncludeBenchmarkPositive) + } +} + +func Benchmark_match3_samedir_positive(b *testing.B) { + for i := 0; i < b.N; i++ { + match3(".include \"options.mk\"", reMkIncludeBenchmarkPositive) + } +} + +func Benchmark_match3_bsd_pkg_mk_comment_positive(b *testing.B) { + for i := 0; i < b.N; i++ { + match3(".include \"../../mk/bsd.pkg.mk\" # infrastructure ", reMkIncludeBenchmarkPositive) + } +} + +func Benchmark_match3_explicit(b *testing.B) { + for i := 0; i < b.N; i++ { + MatchMkInclude(".include \"../../mk/bsd.pkg.mk\" # infrastructure ") + } +} Index: pkgsrc/pkgtools/pkglint/files/main.go diff -u pkgsrc/pkgtools/pkglint/files/main.go:1.8 pkgsrc/pkgtools/pkglint/files/main.go:1.9 --- pkgsrc/pkgtools/pkglint/files/main.go:1.8 Sun Jul 10 21:24:47 2016 +++ pkgsrc/pkgtools/pkglint/files/main.go Mon Nov 14 01:08:23 2016 @@ -4,6 +4,7 @@ import ( "fmt" "io" "os" + "os/user" "path/filepath" "runtime/pprof" ) @@ -61,6 +62,12 @@ func (pkglint *Pkglint) Main(args ...str G.globalData.Initialize() + currentUser, err := user.Current() + if err == nil { + // On Windows, this is `Computername\Username`. + G.CurrentUsername = regcomp(`^.*\\`).ReplaceAllString(currentUser.Username, "") + } + for len(G.Todo) != 0 { item := G.Todo[0] G.Todo = G.Todo[1:] Index: pkgsrc/pkgtools/pkglint/files/mkline.go diff -u pkgsrc/pkgtools/pkglint/files/mkline.go:1.16 pkgsrc/pkgtools/pkglint/files/mkline.go:1.17 --- pkgsrc/pkgtools/pkglint/files/mkline.go:1.16 Tue Nov 1 21:40:25 2016 +++ pkgsrc/pkgtools/pkglint/files/mkline.go Mon Nov 14 01:08:23 2016 @@ -118,7 +118,7 @@ func NewMkLine(line *Line) (mkline *MkLi return } - if m, indent, directive, includefile := match3(text, reMkInclude); m { + if m, indent, directive, includefile := MatchMkInclude(text); m { mkline.xtype = 6 mkline.data = mkLineInclude{directive == "include", indent, includefile, ""} return @@ -1196,6 +1196,14 @@ func (mkline *MkLine) CheckCond() { value := node.args[2].(string) if len(varmods) == 0 { mkline.CheckVartype(varname, opUse, value, "") + if varname == "PKGSRC_COMPILER" { + op := node.args[1].(string) + mkline.Line.Warnf("Use ${PKGSRC_COMPILER:%s%s} instead of the %s operator.", ifelseStr(op == "==", "M", "N"), value, op) + Explain( + "The PKGSRC_COMPILER can be a list of chained compilers, e.g. \"ccache", + "distcc clang\". Therefore, comparing it using == or != leads to", + "wrong results in these cases.") + } } else if len(varmods) == 1 && matches(varmods[0], `^[MN]`) && value != "" { mkline.CheckVartype(varname, opUseMatch, value, "") } Index: pkgsrc/pkgtools/pkglint/files/mkline_test.go diff -u pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.17 pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.18 --- pkgsrc/pkgtools/pkglint/files/mkline_test.go:1.17 Tue Nov 1 21:40:25 2016 +++ pkgsrc/pkgtools/pkglint/files/mkline_test.go Mon Nov 14 01:08:23 2016 @@ -760,7 +760,21 @@ func (s *Suite) Test_MkLine_CheckCond_co G.Mk.Check() - c.Check(s.Output(), equals, "") // Don’t warn about unknown shell command "cc". + // Don’t warn about unknown shell command "cc". + c.Check(s.Output(), equals, "WARN: security/openssl/Makefile:2: Use ${PKGSRC_COMPILER:Mgcc} instead of the == operator.\n") +} + +func (s *Suite) Test_MkLine_CheckCond_comparing_PKGSRC_COMPILER_with_eqeq(c *check.C) { + s.UseCommandLine(c, "-Wall") + G.globalData.InitVartypes() + G.Mk = s.NewMkLines("audio/pulseaudio/Makefile", + mkrcsid, + ".if ${OPSYS} == \"Darwin\" && ${PKGSRC_COMPILER} == \"clang\"", + ".endif") + + G.Mk.Check() + + c.Check(s.Output(), equals, "WARN: audio/pulseaudio/Makefile:2: Use ${PKGSRC_COMPILER:Mclang} instead of the == operator.\n") } func (s *Suite) Test_MkLine_Pkgmandir(c *check.C) { Index: pkgsrc/pkgtools/pkglint/files/vardefs.go diff -u pkgsrc/pkgtools/pkglint/files/vardefs.go:1.17 pkgsrc/pkgtools/pkglint/files/vardefs.go:1.18 --- pkgsrc/pkgtools/pkglint/files/vardefs.go:1.17 Mon Oct 3 12:25:36 2016 +++ pkgsrc/pkgtools/pkglint/files/vardefs.go Mon Nov 14 01:08:23 2016 @@ -504,6 +504,8 @@ func (gd *GlobalData) InitVartypes() { pkglist("NOT_FOR_BULK_PLATFORM", lkSpace, BtMachinePlatformPattern) pkglist("NOT_FOR_PLATFORM", lkSpace, BtMachinePlatformPattern) pkg("NOT_FOR_UNPRIVILEGED", lkNone, BtYesNo) + pkglist("NOT_PAX_ASLR_SAFE", lkShell, BtPathmask) + pkglist("NOT_PAX_MPROTECT_SAFE", lkShell, BtPathmask) acl("NO_BIN_ON_CDROM", lkNone, BtRestricted, "Makefile, Makefile.common: set") acl("NO_BIN_ON_FTP", lkNone, BtRestricted, "Makefile, Makefile.common: set") acl("NO_BUILD", lkNone, BtYes, "Makefile, Makefile.common: set; Makefile.*: default, set") Index: pkgsrc/pkgtools/pkglint/files/package.go diff -u pkgsrc/pkgtools/pkglint/files/package.go:1.12 pkgsrc/pkgtools/pkglint/files/package.go:1.13 --- pkgsrc/pkgtools/pkglint/files/package.go:1.12 Tue Nov 1 21:40:25 2016 +++ pkgsrc/pkgtools/pkglint/files/package.go Mon Nov 14 01:08:23 2016 @@ -2,7 +2,6 @@ package main import ( "fmt" - "os/user" "path" "regexp" "strconv" @@ -329,11 +328,13 @@ func (pkg *Package) readMakefile(fname s // current file and in the current working directory. // Pkglint doesn’t have an include dir list, like make(1) does. if !fileExists(dirname + "/" + includeFile) { - dirname = G.CurrentDir - } - if !fileExists(dirname + "/" + includeFile) { - line.Error1("Cannot read %q.", dirname+"/"+includeFile) - return false + if dirname != G.CurrentDir { // Prevent unnecessary syscalls + dirname = G.CurrentDir + if !fileExists(dirname + "/" + includeFile) { + line.Error1("Cannot read %q.", dirname+"/"+includeFile) + return false + } + } } if G.opts.Debug { @@ -808,13 +809,7 @@ func (pkg *Package) checkLocallyModified return } - user, err := user.Current() - if err != nil || user.Username == "" { - return - } - // On Windows, this is `Computername\Username`. - username := regcomp(`^.*\\`).ReplaceAllString(user.Username, "") - + username := G.CurrentUsername if G.opts.Debug { traceStep("user=%q owner=%q maintainer=%q", username, owner, maintainer) } Index: pkgsrc/pkgtools/pkglint/files/pkglint.go diff -u pkgsrc/pkgtools/pkglint/files/pkglint.go:1.13 pkgsrc/pkgtools/pkglint/files/pkglint.go:1.14 --- pkgsrc/pkgtools/pkglint/files/pkglint.go:1.13 Tue Nov 1 21:40:25 2016 +++ pkgsrc/pkgtools/pkglint/files/pkglint.go Mon Nov 14 01:08:23 2016 @@ -7,8 +7,7 @@ import ( ) const ( - reMkInclude = `^\.(\s*)(s?include)\s+\"([^\"]+)\"\s*(?:#.*)?$` - rePkgname = `^([\w\-.+]+)-(\d(?:\w|\.\d)*)$` + rePkgname = `^([\w\-.+]+)-(\d(?:\w|\.\d)*)$` ) // Returns the pkgsrc top-level directory, relative to the given file or directory. @@ -386,6 +385,66 @@ func MatchVarassign(text string) (m bool return } +func MatchMkInclude(text string) (bool, string, string, string) { + goto start +fail: + return false, "", "", "" + +start: + len := len(text) + if len == 0 || text[0] != '.' { + goto fail + } + + i := 1 + + spaceStart := i + for i < len && (text[i] == ' ' || text[i] == '\t') { + i++ + } + spaceEnd := i + + directiveStart := i + if i < len && text[i] == 's' { + i++ + } + if !(i+7 < len && text[i] == 'i' && text[i+1] == 'n' && text[i+2] == 'c' && text[i+3] == 'l' && text[i+4] == 'u' && text[i+5] == 'd' && text[i+6] == 'e') { + goto fail + } + i += 7 + directiveEnd := i + + for i < len && (text[i] == ' ' || text[i] == '\t') { + i++ + } + if !(i < len && text[i] == '"') { + goto fail + } + i++ + + pathStart := i + for i < len && text[i] != '"' { + i++ + } + pathEnd := i + if !(pathStart < pathEnd) { + goto fail + } + + if !(i < len && text[i] == '"') { + goto fail + } + i++ + + for i < len && (text[i] == ' ' || text[i] == '\t') { + i++ + } + if !(i == len || i < len && text[i] == '#') { + goto fail + } + return true, text[spaceStart:spaceEnd], text[directiveStart:directiveEnd], text[pathStart:pathEnd] +} + type DependencyPattern struct { pkgbase string // "freeciv-client", "{gcc48,gcc48-libs}", "${EMACS_REQD}" lowerOp string // ">=", ">" Index: pkgsrc/pkgtools/pkglint/files/plist_test.go diff -u pkgsrc/pkgtools/pkglint/files/plist_test.go:1.9 pkgsrc/pkgtools/pkglint/files/plist_test.go:1.10 --- pkgsrc/pkgtools/pkglint/files/plist_test.go:1.9 Tue Nov 1 21:40:25 2016 +++ pkgsrc/pkgtools/pkglint/files/plist_test.go Mon Nov 14 01:08:23 2016 @@ -221,7 +221,7 @@ func (s *Suite) Test_PlistChecker__autof "AUTOFIX: ~/PLIST: Has been auto-fixed. Please re-run pkglint.\n") c.Check(len(lines), equals, len(fixedLines)) c.Check(s.LoadTmpFile(c, "PLIST"), equals, ""+ - "@comment $NetBSD: plist_test.go,v 1.9 2016/11/01 21:40:25 rillig Exp $\n"+ + "@comment $"+"NetBSD$\n"+ "${PLIST.xen}lib/libvirt/connection-driver/libvirt_driver_libxl.la\n"+ "${PLIST.hal}lib/libvirt/connection-driver/libvirt_driver_nodedev.la\n"+ "lib/libvirt/connection-driver/libvirt_driver_storage.la\n"+ Index: pkgsrc/pkgtools/pkglint/files/util.go diff -u pkgsrc/pkgtools/pkglint/files/util.go:1.9 pkgsrc/pkgtools/pkglint/files/util.go:1.10 --- pkgsrc/pkgtools/pkglint/files/util.go:1.9 Sat Jul 9 09:43:48 2016 +++ pkgsrc/pkgtools/pkglint/files/util.go Mon Nov 14 01:08:23 2016 @@ -71,13 +71,10 @@ func getSubdirs(fname string) []string { // Checks whether a file is already committed to the CVS repository. func isCommitted(fname string) bool { - basename := path.Base(fname) - lines, err := readLines(path.Dir(fname)+"/CVS/Entries", false) - if err != nil { - return false - } + lines := loadCvsEntries(fname) + needle := "/" + path.Base(fname) + "/" for _, line := range lines { - if hasPrefix(line.Text, "/"+basename+"/") { + if hasPrefix(line.Text, needle) { return true } } @@ -85,13 +82,10 @@ func isCommitted(fname string) bool { } func isLocallyModified(fname string) bool { - basename := path.Base(fname) - lines, err := readLines(path.Dir(fname)+"/CVS/Entries", false) - if err != nil { - return false - } + lines := loadCvsEntries(fname) + needle := "/" + path.Base(fname) + "/" for _, line := range lines { - if hasPrefix(line.Text, "/"+basename+"/") { + if hasPrefix(line.Text, needle) { cvsModTime, err := time.Parse(time.ANSIC, strings.Split(line.Text, "/")[3]) if err != nil { return false @@ -112,6 +106,21 @@ func isLocallyModified(fname string) boo return false } +func loadCvsEntries(fname string) []*Line { + dir := path.Dir(fname) + if dir == G.CvsEntriesDir { + return G.CvsEntriesLines + } + + lines, err := readLines(dir+"/CVS/Entries", false) + if err != nil { + return nil + } + G.CvsEntriesDir = dir + G.CvsEntriesLines = lines + return lines +} + // Returns the number of columns that a string occupies when printed with // a tabulator size of 8. func tabLength(s string) int { --_----------=_1479085703230680--