From 6b1357482795e331d3a093cc45e693ccffde9323 Mon Sep 17 00:00:00 2001
From: Joey Hess <joeyh@joeyh.name>
Date: Tue, 15 Dec 2020 12:39:34 -0400
Subject: [PATCH] Windows: include= and exclude= containing '/' will also match
 filenames that are written using '\'

And vice-versa, but it's better to use '/' for portability.

Notably, standardPreferredContent contains "archive/*" and that might not
match if the filename ends up coming in with the slashes the other way
around.
---
 Annex/MetaData.hs                             |  2 +-
 Annex/View.hs                                 |  6 ++--
 CHANGELOG                                     |  3 ++
 Limit.hs                                      |  4 +--
 Types/RefSpec.hs                              |  6 ++--
 Utility/Glob.hs                               | 34 ++++++++++++++-----
 ..._9f0415b3945ee80698b5ee50351f1a5c._comment | 11 ++++++
 7 files changed, 49 insertions(+), 17 deletions(-)
 create mode 100644 doc/bugs/drop_claims_that_content_is_required___40__8.20201127__41__/comment_3_9f0415b3945ee80698b5ee50351f1a5c._comment

diff --git a/Annex/MetaData.hs b/Annex/MetaData.hs
index 4e0a541af9..e821101c0a 100644
--- a/Annex/MetaData.hs
+++ b/Annex/MetaData.hs
@@ -107,7 +107,7 @@ parseMetaDataMatcher p = (,)
 		('>':v) -> checkcmp (>) v
 		_ -> checkglob ""
 	checkglob v =
-		let cglob = compileGlob v CaseInsensative
+		let cglob = compileGlob v CaseInsensative (GlobFilePath False)
 		in matchGlob cglob . decodeBS . fromMetaValue
 	checkcmp cmp v v' = case (doubleval v, doubleval (decodeBS (fromMetaValue v'))) of
 		(Just d, Just d') -> d' `cmp` d
diff --git a/Annex/View.hs b/Annex/View.hs
index f13c92909f..9cbf9110fc 100644
--- a/Annex/View.hs
+++ b/Annex/View.hs
@@ -163,11 +163,11 @@ combineViewFilter old@(ExcludeValues olds) (ExcludeValues news)
 combineViewFilter (FilterValues _) newglob@(FilterGlob _) =
 	(newglob, Widening)
 combineViewFilter (FilterGlob oldglob) new@(FilterValues s)
-	| all (matchGlob (compileGlob oldglob CaseInsensative) . decodeBS . fromMetaValue) (S.toList s) = (new, Narrowing)
+	| all (matchGlob (compileGlob oldglob CaseInsensative (GlobFilePath False)) . decodeBS . fromMetaValue) (S.toList s) = (new, Narrowing)
 	| otherwise = (new, Widening)
 combineViewFilter (FilterGlob old) newglob@(FilterGlob new)
 	| old == new = (newglob, Unchanged)
-	| matchGlob (compileGlob old CaseInsensative) new = (newglob, Narrowing)
+	| matchGlob (compileGlob old CaseInsensative (GlobFilePath False)) new = (newglob, Narrowing)
 	| otherwise = (newglob, Widening)
 combineViewFilter (FilterGlob _) new@(ExcludeValues _) = (new, Narrowing)
 combineViewFilter (ExcludeValues _) new@(FilterGlob _) = (new, Widening)
@@ -216,7 +216,7 @@ viewComponentMatcher viewcomponent = \metadata ->
 		FilterValues s -> \values -> setmatches $
 			S.intersection s values
 		FilterGlob glob ->
-			let cglob = compileGlob glob CaseInsensative
+			let cglob = compileGlob glob CaseInsensative (GlobFilePath False)
 			in \values -> setmatches $
 				S.filter (matchGlob cglob . decodeBS . fromMetaValue) values
 		ExcludeValues excludes -> \values -> 
diff --git a/CHANGELOG b/CHANGELOG
index aaaa508b71..2da82ed5b9 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -26,6 +26,9 @@ git-annex (8.20201128) UNRELEASED; urgency=medium
     enclosure, but only a link to an url which youtube-dl does not support.
   * initremote: Prevent enabling encryption with exporttree=yes or 
     importtree=yes.
+  * Windows: include= and exclude= containing '/' will also match filenames
+    that are written using '\'. (And vice-versa, but it's better to use '/'
+    for portability.)
 
  -- Joey Hess <id@joeyh.name>  Mon, 30 Nov 2020 12:55:49 -0400
 
diff --git a/Limit.hs b/Limit.hs
index 2e03e86a99..6da8cd0c49 100644
--- a/Limit.hs
+++ b/Limit.hs
@@ -115,7 +115,7 @@ limitExclude glob = Right $ MatchFiles
 matchGlobFile :: String -> MatchInfo -> Annex Bool
 matchGlobFile glob = go
   where
-	cglob = compileGlob glob CaseSensative -- memoized
+	cglob = compileGlob glob CaseSensative (GlobFilePath True) -- memoized
 	go (MatchingFile fi) = pure $ matchGlob cglob (fromRawFilePath (matchFile fi))
 	go (MatchingInfo p) = pure $ matchGlob cglob (fromRawFilePath (providedFilePath p))
 	go (MatchingUserInfo p) = matchGlob cglob <$> getUserInfo (userProvidedFilePath p)
@@ -166,7 +166,7 @@ matchMagic _limitname querymagic selectprovidedinfo selectuserprovidedinfo (Just
 		, matchNeedsLocationLog = False
 		}
   where
- 	cglob = compileGlob glob CaseSensative -- memoized
+ 	cglob = compileGlob glob CaseSensative (GlobFilePath False) -- memoized
 	go (MatchingKey _ _) = pure False
 	go (MatchingFile fi) = case contentFile fi of
 		Just f -> catchBoolIO $
diff --git a/Types/RefSpec.hs b/Types/RefSpec.hs
index 0f3dded9d9..0567622319 100644
--- a/Types/RefSpec.hs
+++ b/Types/RefSpec.hs
@@ -22,7 +22,7 @@ data RefSpecPart
 	| RemoveMatching Glob
 
 allRefSpec :: RefSpec
-allRefSpec = [AddMatching $ compileGlob "*" CaseSensative]
+allRefSpec = [AddMatching $ compileGlob "*" CaseSensative (GlobFilePath False)]
 
 parseRefSpec :: String -> Either String RefSpec
 parseRefSpec v = case partitionEithers (map mk $ splitc ':' v) of
@@ -31,9 +31,9 @@ parseRefSpec v = case partitionEithers (map mk $ splitc ':' v) of
   where
 	mk ('+':s)
 		| any (`elem` s) "*?" =
-			Right $ AddMatching $ compileGlob s CaseSensative
+			Right $ AddMatching $ compileGlob s CaseSensative (GlobFilePath False)
 		| otherwise = Right $ AddRef $ Ref $ encodeBS s
-	mk ('-':s) = Right $ RemoveMatching $ compileGlob s CaseSensative
+	mk ('-':s) = Right $ RemoveMatching $ compileGlob s CaseSensative (GlobFilePath False)
 	mk "reflog" = Right AddRefLog
 	mk s = Left $ "bad refspec item \"" ++ s ++ "\" (expected + or - prefix)"
 
diff --git a/Utility/Glob.hs b/Utility/Glob.hs
index c7d535933d..9f2d147b4c 100644
--- a/Utility/Glob.hs
+++ b/Utility/Glob.hs
@@ -1,15 +1,17 @@
-{-# LANGUAGE PackageImports #-}
-
 {- file globbing
  -
- - Copyright 2014 Joey Hess <id@joeyh.name>
+ - Copyright 2014-2020 Joey Hess <id@joeyh.name>
  -
  - License: BSD-2-clause
  -}
 
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE PackageImports #-}
+
 module Utility.Glob (
 	Glob,
 	GlobCase(..),
+	GlobFilePath(..),
 	compileGlob,
 	matchGlob
 ) where
@@ -24,26 +26,42 @@ newtype Glob = Glob Regex
 
 data GlobCase = CaseSensative | CaseInsensative
 
+-- Is the glob being used to match filenames? 
+--
+-- When matching filenames,
+-- a single path separator (eg /) in the glob will match any
+-- number of path separators in the filename.
+-- And on Windows, both / and \ are used as path separators, so compile
+-- the glob to a regexp that matches either path separator.
+newtype GlobFilePath = GlobFilePath Bool
+
 {- Compiles a glob to a regex, that can be repeatedly used. -}
-compileGlob :: String -> GlobCase -> Glob
-compileGlob glob globcase = Glob $
+compileGlob :: String -> GlobCase -> GlobFilePath -> Glob
+compileGlob glob globcase globfilepath = Glob $
 	case compile (defaultCompOpt {caseSensitive = casesentitive}) defaultExecOpt regex of
 		Right r -> r
 		Left _ -> giveup $ "failed to compile regex: " ++ regex
   where
-	regex = '^' : wildToRegex glob ++ "$"
+	regex = '^' : wildToRegex globfilepath glob ++ "$"
 	casesentitive = case globcase of
 		CaseSensative -> True
 		CaseInsensative -> False
 
-wildToRegex :: String -> String
-wildToRegex = concat . go
+wildToRegex :: GlobFilePath -> String -> String
+wildToRegex (GlobFilePath globfile) = concat . go
   where
 	go [] = []
 	go ('*':xs) = ".*" : go xs
 	go ('?':xs) = "." : go xs
 	go ('[':'!':xs) = "[^" : inpat xs
 	go ('[':xs) = "[" : inpat xs
+#ifdef mingw32_HOST_OS
+	go ('/':xs) | globfile = "[/\\]+" : go xs
+	go ('\\':xs) | globfile = "[/\\]+" : go xs
+#else
+	go ('/':xs) | globfile = "[/]+" : go xs
+	go ('\\':xs) | globfile = "[\\]+" : go xs
+#endif
 	go (x:xs)
 		| isDigit x || isAlpha x = [x] : go xs
 		| otherwise = esc x : go xs
diff --git a/doc/bugs/drop_claims_that_content_is_required___40__8.20201127__41__/comment_3_9f0415b3945ee80698b5ee50351f1a5c._comment b/doc/bugs/drop_claims_that_content_is_required___40__8.20201127__41__/comment_3_9f0415b3945ee80698b5ee50351f1a5c._comment
new file mode 100644
index 0000000000..b0e38eb212
--- /dev/null
+++ b/doc/bugs/drop_claims_that_content_is_required___40__8.20201127__41__/comment_3_9f0415b3945ee80698b5ee50351f1a5c._comment
@@ -0,0 +1,11 @@
+[[!comment format=mdwn
+ username="joey"
+ subject="""comment 3"""
+ date="2020-12-15T16:16:21Z"
+ content="""
+Hmm, yes preferred content expressions on windows ought
+to let `/` be used and still match on `\`. (And vice-versa, although then
+the preferred content expression is windows-specific.)
+
+I've implemented that now.
+"""]]