diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py --- a/mercurial/cmdutil.py +++ b/mercurial/cmdutil.py @@ -400,6 +400,178 @@ def dorecord(ui, repo, commitfunc, cmdsu return commit(ui, repo, recordinwlock, pats, opts) +def tersestatus(root, statlist, status, ignorefn, ignore): + """ + Returns a list of statuses with directory collapsed if all the files in the + directory has the same status. + """ + + def numfiles(dirname): + """ + Calculates the number of tracked files in a given directory which also + includes files which were removed or deleted. Considers ignored files + if ignore argument is True or 'i' is present in status argument. + """ + if lencache.get(dirname): + return lencache[dirname] + if 'i' in status or ignore: + def match(localpath): + absolutepath = os.path.join(root, localpath) + if os.path.isdir(absolutepath) and isemptydir(absolutepath): + return True + return False + else: + def match(localpath): + # there can be directory whose all the files are ignored and + # hence the drectory should also be ignored while counting + # number of files or subdirs in it's parent directory. This + # checks the same. + # XXX: We need a better logic here. + if os.path.isdir(os.path.join(root, localpath)): + return isignoreddir(localpath) + else: + # XXX: there can be files which have the ignored pattern but + # are not ignored. That leads to bug in counting number of + # tracked files in the directory. + return ignorefn(localpath) + lendir = 0 + abspath = os.path.join(root, dirname) + # There might be cases when a directory does not exists as the whole + # directory can be removed and/or deleted. + try: + for f in os.listdir(abspath): + localpath = os.path.join(dirname, f) + if not match(localpath): + lendir += 1 + except OSError: + pass + lendir += len(absentdir.get(dirname, [])) + lencache[dirname] = lendir + return lendir + + def isemptydir(abspath): + """ + Check whether a directory is empty or not, i.e. there is no files in the + directory and all its subdirectories. + """ + for f in os.listdir(abspath): + fullpath = os.path.join(abspath, f) + if os.path.isdir(fullpath): + # recursion here + ret = isemptydir(fullpath) + if not ret: + return False + else: + return False + return True + + def isignoreddir(localpath): + """ + This function checks whether the directory contains only ignored files + and hence should the directory be considered ignored. Returns True, if + that should be ignored otherwise False. + """ + dirpath = os.path.join(root, localpath) + for f in os.listdir(dirpath): + filepath = os.path.join(dirpath, f) + if os.path.isdir(filepath): + # recursion here + ret = isignoreddir(os.path.join(localpath, f)) + if not ret: + return False + else: + if not ignorefn(os.path.join(localpath, f)): + return False + return True + + def absentones(removedfiles, missingfiles): + """ + Returns a dictionary of directories with files in it which are either + removed or missing (deleted) in them. + """ + absentdir = {} + absentfiles = removedfiles + missingfiles + while absentfiles: + f = absentfiles.pop() + par = os.path.dirname(f) + if par == '': + continue + # we need to store files rather than number of files as some files + # or subdirectories in a directory can be counted twice. This is + # also we have used sets here. + try: + absentdir[par].add(f) + except KeyError: + absentdir[par] = set([f]) + absentfiles.append(par) + return absentdir + + indexes = {'m': 0, 'a': 1, 'r': 2, 'd': 3, 'u': 4, 'i': 5, 'c': 6} + # get a dictonary of directories and files which are missing as os.listdir() + # won't be able to list them. + absentdir = absentones(statlist[2], statlist[3]) + finalrs = [[]] * len(indexes) + didsomethingchanged = False + # dictionary to store number of files and subdir in a directory so that we + # don't compute that again. + lencache = {} + + for st in pycompat.bytestr(status): + + try: + ind = indexes[st] + except KeyError: + # TODO: Need a better error message here + raise error.Abort("'%s' not recognized" % st) + + sfiles = statlist[ind] + if not sfiles: + continue + pardict = {} + for a in sfiles: + par = os.path.dirname(a) + pardict.setdefault(par, []).append(a) + + rs = [] + newls = [] + for par, files in pardict.iteritems(): + lenpar = numfiles(par) + if lenpar == len(files): + newls.append(par) + + if not newls: + continue + + while newls: + newel = newls.pop() + if newel == '': + continue + parn = os.path.dirname(newel) + pardict[newel] = [] + # Adding pycompat.ossep as newel is a directory. + pardict.setdefault(parn, []).append(newel + pycompat.ossep) + lenpar = numfiles(parn) + if lenpar == len(pardict[parn]): + newls.append(parn) + + # dict.values() for Py3 compatibility + for files in pardict.values(): + rs.extend(files) + + rs.sort() + finalrs[ind] = rs + didsomethingchanged = True + + # If nothing is changed, make sure the order of files is preserved. + if not didsomethingchanged: + return statlist + + for x in xrange(len(indexes)): + if not finalrs[x]: + finalrs[x] = statlist[x] + + return finalrs + def findpossible(cmd, table, strict=False): """ Return cmd -> (aliases, command table entry) diff --git a/mercurial/commands.py b/mercurial/commands.py --- a/mercurial/commands.py +++ b/mercurial/commands.py @@ -4617,6 +4617,7 @@ def serve(ui, repo, **opts): ('u', 'unknown', None, _('show only unknown (not tracked) files')), ('i', 'ignored', None, _('show only ignored files')), ('n', 'no-status', None, _('hide status prefix')), + ('t', 'terse', '', _('show the terse output (EXPERIMENTAL)')), ('C', 'copies', None, _('show source of copied files')), ('0', 'print0', None, _('end filenames with NUL, for use with xargs')), ('', 'rev', [], _('show difference from revision'), _('REV')), @@ -4662,6 +4663,16 @@ def status(ui, repo, *pats, **opts): .. container:: verbose + The -t/--terse option abbreviates the output by showing directory name + if all the files in it share the same status. The option expects a value + which can be a string formed by using 'm', 'a', 'r', 'd', 'u', 'i', 'c' + where, 'm' stands for 'modified', 'a' for 'added', 'r' for 'removed', + 'd' for 'deleted', 'u' for 'unknown', 'i' for 'ignored' and 'c' for clean. + + It terses the output of only those status which are passed. The ignored + files are not considered while tersing until 'i' is there in --terse value + or the --ignored option is used. + Examples: - show changes in the working directory relative to a @@ -4688,10 +4699,14 @@ def status(ui, repo, *pats, **opts): opts = pycompat.byteskwargs(opts) revs = opts.get('rev') change = opts.get('change') + terse = opts.get('terse') if revs and change: msg = _('cannot specify --rev and --change at the same time') raise error.Abort(msg) + elif revs and terse: + msg = _('cannot use --terse with --rev') + raise error.Abort(msg) elif change: node2 = scmutil.revsingle(repo, change, None).node() node1 = repo[node2].p1().node() @@ -4712,6 +4727,7 @@ def status(ui, repo, *pats, **opts): show = [k for k in states if opts.get(k)] if opts.get('all'): show += ui.quiet and (states[:4] + ['clean']) or states + if not show: if ui.quiet: show = states[:4] @@ -4722,6 +4738,9 @@ def status(ui, repo, *pats, **opts): stat = repo.status(node1, node2, m, 'ignored' in show, 'clean' in show, 'unknown' in show, opts.get('subrepos')) + if terse: + stat = cmdutil.tersestatus(repo.root, stat, terse, + repo.dirstate._ignore, opts.get('ignored')) changestates = zip(states, pycompat.iterbytestr('MAR!?IC'), stat) if (opts.get('all') or opts.get('copies') diff --git a/tests/test-completion.t b/tests/test-completion.t --- a/tests/test-completion.t +++ b/tests/test-completion.t @@ -231,7 +231,7 @@ Show all commands + options push: force, rev, bookmark, branch, new-branch, ssh, remotecmd, insecure remove: after, force, subrepos, include, exclude serve: accesslog, daemon, daemon-postexec, errorlog, port, address, prefix, name, web-conf, webdir-conf, pid-file, stdio, cmdserver, templates, style, ipv6, certificate, subrepos - status: all, modified, added, removed, deleted, clean, unknown, ignored, no-status, copies, print0, rev, change, include, exclude, subrepos, template + status: all, modified, added, removed, deleted, clean, unknown, ignored, no-status, terse, copies, print0, rev, change, include, exclude, subrepos, template summary: remote update: clean, check, merge, date, rev, tool addremove: similarity, subrepos, include, exclude, dry-run diff --git a/tests/test-terse-status.t b/tests/test-terse-status.t new file mode 100644 --- /dev/null +++ b/tests/test-terse-status.t @@ -0,0 +1,185 @@ + $ mkdir folder + $ cd folder + $ hg init + $ mkdir x x/l x/m x/n x/l/u x/l/u/a + $ touch a b x/aa.o x/bb.o + $ hg status + ? a + ? b + ? x/aa.o + ? x/bb.o + + $ hg status --terse u + ? a + ? b + ? x/ + $ hg status --terse maudric + ? a + ? b + ? x/ + $ hg status --terse madric + ? a + ? b + ? x/aa.o + ? x/bb.o + $ hg status --terse f + abort: 'f' not recognized + [255] + +Add a .hgignore so that we can also have ignored files + + $ echo ".*\.o" > .hgignore + $ hg status + ? .hgignore + ? a + ? b + $ hg status -i + I x/aa.o + I x/bb.o + +Tersing ignored files + $ hg status -t i --ignored + I x/ + +Adding more files + $ mkdir y + $ touch x/aa x/bb y/l y/m y/l.o y/m.o + $ touch x/l/aa x/m/aa x/n/aa x/l/u/bb x/l/u/a/bb + + $ hg status + ? .hgignore + ? a + ? b + ? x/aa + ? x/bb + ? x/l/aa + ? x/l/u/a/bb + ? x/l/u/bb + ? x/m/aa + ? x/n/aa + ? y/l + ? y/m + + $ hg status --terse u + ? .hgignore + ? a + ? b + ? x/ + ? y/ + + $ hg add x/aa x/bb .hgignore + $ hg status --terse au + A .hgignore + A x/aa + A x/bb + ? a + ? b + ? x/l/ + ? x/m/ + ? x/n/ + ? y/ + +Including ignored files + + $ hg status --terse aui + A .hgignore + A x/aa + A x/bb + ? a + ? b + ? x/l/ + ? x/m/ + ? x/n/ + ? y/l + ? y/m + $ hg status --terse au -i + I x/aa.o + I x/bb.o + I y/l.o + I y/m.o + +Committing some of the files + + $ hg commit x/aa x/bb .hgignore -m "First commit" + $ hg status + ? a + ? b + ? x/l/aa + ? x/l/u/a/bb + ? x/l/u/bb + ? x/m/aa + ? x/n/aa + ? y/l + ? y/m + $ hg status --terse mardu + ? a + ? b + ? x/l/ + ? x/m/ + ? x/n/ + ? y/ + +Modifying already committed files + + $ echo "Hello" >> x/aa + $ echo "World" >> x/bb + $ hg status --terse maurdc + M x/aa + M x/bb + ? a + ? b + ? x/l/ + ? x/m/ + ? x/n/ + ? y/ + +Respecting other flags + + $ hg status --terse marduic --all + M x/aa + M x/bb + ? a + ? b + ? x/l/ + ? x/m/ + ? x/n/ + ? y/l + ? y/m + I x/aa.o + I x/bb.o + I y/l.o + I y/m.o + C .hgignore + $ hg status --terse marduic -a + $ hg status --terse marduic -c + C .hgignore + $ hg status --terse marduic -m + M x/aa + M x/bb + +Passing 'i' in terse value will consider the ignored files while tersing + + $ hg status --terse marduic -u + ? a + ? b + ? x/l/ + ? x/m/ + ? x/n/ + ? y/l + ? y/m + +Omitting 'i' in terse value does not consider ignored files while tersing + + $ hg status --terse marduc -u + ? a + ? b + ? x/l/ + ? x/m/ + ? x/n/ + ? y/ + +Trying with --rev + + $ hg status --terse marduic --rev 0 --rev 1 + abort: cannot use --terse with --rev + [255]