2 {-# LANGUAGE LambdaCase #-}
3 {-# LANGUAGE NamedFieldPuns #-}
4 {-# LANGUAGE RankNTypes #-}
5 {-# LANGUAGE RecordWildCards #-}
6 {-# LANGUAGE ScopedTypeVariables #-}
8 module Distribution
.Client
.VCS
19 -- * Validating 'SourceRepo's and configuring VCS drivers
20 , validatePDSourceRepo
23 , SourceRepoProblem
(..)
27 -- * Running the VCS driver
31 -- * The individual VCS drivers
41 import Distribution
.Client
.Compat
.Prelude
44 import Distribution
.Client
.RebuildMonad
47 , monitorDirectoryExistence
50 import Distribution
.Client
.Types
.SourceRepo
(SourceRepoMaybe
, SourceRepositoryPackage
(..), srpToProxy
)
51 import qualified Distribution
.PackageDescription
as PD
52 import Distribution
.Simple
.Program
53 ( ConfiguredProgram
(programVersion
)
54 , Program
(programFindVersion
)
55 , ProgramInvocation
(..)
58 , getProgramInvocationOutput
61 , runProgramInvocation
64 import Distribution
.Simple
.Program
.Db
65 ( prependProgramSearchPath
67 import Distribution
.Types
.SourceRepo
71 import Distribution
.Verbosity
as Verbosity
74 import Distribution
.Version
78 #if !MIN_VERSION_base
(4,18,0)
79 import Control
.Applicative
83 import Control
.Exception
87 import Control
.Monad
.Trans
90 import qualified Data
.Char as Char
91 import qualified Data
.List
as List
92 import qualified Data
.Map
as Map
93 import System
.Directory
95 , removeDirectoryRecursive
97 import System
.FilePath
101 import System
.IO.Error
102 ( isDoesNotExistError
105 -- | A driver for a version control system, e.g. git, darcs etc.
106 data VCS program
= VCS
107 { vcsRepoType
:: RepoType
108 -- ^ The type of repository this driver is for.
109 , vcsProgram
:: program
110 -- ^ The vcs program itself.
111 -- This is used at type 'Program' and 'ConfiguredProgram'.
116 -> SourceRepositoryPackage f
117 -> FilePath -- Source URI
118 -> FilePath -- Destination directory
119 -> [ProgramInvocation
]
120 -- ^ The program invocation(s) to get\/clone a repository into a fresh
126 -> [(SourceRepositoryPackage f
, FilePath)]
127 -> IO [MonitorFilePath
]
128 -- ^ The program invocation(s) to synchronise a whole set of /related/
129 -- repositories with corresponding local directories. Also returns the
130 -- files that the command depends on, for change monitoring.
133 -- ------------------------------------------------------------
135 -- * Selecting repos and drivers
137 -- ------------------------------------------------------------
139 data SourceRepoProblem
140 = SourceRepoRepoTypeUnspecified
141 | SourceRepoRepoTypeUnsupported
(SourceRepositoryPackage Proxy
) RepoType
142 | SourceRepoLocationUnspecified
145 -- | Validates that the 'SourceRepo' specifies a location URI and a repository
146 -- type that is supported by a VCS driver.
148 -- | It also returns the 'VCS' driver we should use to work with it.
150 :: SourceRepositoryPackage f
151 -> Either SourceRepoProblem
(SourceRepositoryPackage f
, String, RepoType
, VCS Program
)
152 validateSourceRepo
= \repo
-> do
153 let rtype
= srpType repo
154 vcs
<- Map
.lookup rtype knownVCSs ?
! SourceRepoRepoTypeUnsupported
(srpToProxy repo
) rtype
155 let uri
= srpLocation repo
156 return (repo
, uri
, rtype
, vcs
)
158 a ?
! e
= maybe (Left e
) Right a
162 -> Either SourceRepoProblem
(SourceRepoMaybe
, String, RepoType
, VCS Program
)
163 validatePDSourceRepo repo
= do
164 rtype
<- PD
.repoType repo ?
! SourceRepoRepoTypeUnspecified
165 uri
<- PD
.repoLocation repo ?
! SourceRepoLocationUnspecified
167 SourceRepositoryPackage
170 , srpTag
= PD
.repoTag repo
171 , srpBranch
= PD
.repoBranch repo
172 , srpSubdir
= PD
.repoSubdir repo
173 , srpCommand
= mempty
176 a ?
! e
= maybe (Left e
) Right a
178 -- | As 'validateSourceRepo' but for a bunch of 'SourceRepo's, and return
179 -- things in a convenient form to pass to 'configureVCSs', or to report
182 :: [SourceRepositoryPackage f
]
184 [(SourceRepositoryPackage f
, SourceRepoProblem
)]
185 [(SourceRepositoryPackage f
, String, RepoType
, VCS Program
)]
186 validateSourceRepos rs
=
187 case partitionEithers
(map validateSourceRepo
' rs
) of
188 (problems
@(_
: _
), _
) -> Left problems
189 ([], vcss
) -> Right vcss
192 :: SourceRepositoryPackage f
194 (SourceRepositoryPackage f
, SourceRepoProblem
)
195 (SourceRepositoryPackage f
, String, RepoType
, VCS Program
)
196 validateSourceRepo
' r
=
200 (validateSourceRepo r
)
205 -- ^ Extra prog paths
207 -> IO (VCS ConfiguredProgram
)
208 configureVCS verbosity progPaths vcs
@VCS
{vcsProgram
= prog
} = do
209 progPath
<- prependProgramSearchPath verbosity progPaths
[] emptyProgramDb
210 asVcsConfigured
<$> requireProgram verbosity prog progPath
212 asVcsConfigured
(prog
', _
) = vcs
{vcsProgram
= prog
'}
217 -- ^ Extra prog paths
218 -> Map RepoType
(VCS Program
)
219 -> IO (Map RepoType
(VCS ConfiguredProgram
))
220 configureVCSs verbosity progPaths
= traverse
(configureVCS verbosity progPaths
)
222 -- ------------------------------------------------------------
224 -- * Running the driver
226 -- ------------------------------------------------------------
228 -- | Clone a single source repo into a fresh directory, using a configured VCS.
230 -- This is for making a new copy, not synchronising an existing copy. It will
231 -- fail if the destination directory already exists.
233 -- Make sure to validate the 'SourceRepo' using 'validateSourceRepo' first.
236 -> VCS ConfiguredProgram
237 -> SourceRepositoryPackage f
243 repo
@SourceRepositoryPackage
{srpLocation
= srcuri
}
245 traverse_
(runProgramInvocation verbosity
) invocations
256 -- | Synchronise a set of 'SourceRepo's referring to the same repository with
257 -- corresponding local directories. The local directories may or may not
260 -- The 'SourceRepo' values used in a single invocation of 'syncSourceRepos',
261 -- or used across a series of invocations with any local directory must refer
262 -- to the /same/ repository. That means it must be the same location but they
263 -- can differ in the branch, or tag or subdir.
265 -- The reason to allow multiple related 'SourceRepo's is to allow for the
266 -- network or storage to be shared between different checkouts of the repo.
267 -- For example if a single repo contains multiple packages in different subdirs
268 -- and in some project it may make sense to use a different state of the repo
269 -- for one subdir compared to another.
272 -> VCS ConfiguredProgram
273 -> [(SourceRepositoryPackage f
, FilePath)]
275 syncSourceRepos verbosity vcs repos
= do
276 files
<- liftIO
$ vcsSyncRepos vcs verbosity
(vcsProgram vcs
) repos
279 -- ------------------------------------------------------------
281 -- * The various VCS drivers
283 -- ------------------------------------------------------------
285 -- | The set of all supported VCS drivers, organised by 'RepoType'.
286 knownVCSs
:: Map RepoType
(VCS Program
)
287 knownVCSs
= Map
.fromList
[(vcsRepoType vcs
, vcs
) | vcs
<- vcss
]
289 vcss
= [vcsBzr
, vcsDarcs
, vcsGit
, vcsHg
, vcsSvn
]
291 -- | VCS driver for Bazaar.
292 vcsBzr
:: VCS Program
295 { vcsRepoType
= KnownRepoType Bazaar
296 , vcsProgram
= bzrProgram
304 -> SourceRepositoryPackage f
307 -> [ProgramInvocation
]
308 vcsCloneRepo verbosity prog repo srcuri destdir
=
311 ([branchCmd
, srcuri
, destdir
] ++ tagArgs
++ verboseArg
)
314 -- The @get@ command was deprecated in version 2.4 in favour of
315 -- the alias @branch@
317 | programVersion prog
>= Just
(mkVersion
[2, 4]) =
322 tagArgs
= case srpTag repo
of
324 Just tag
-> ["-r", "tag:" ++ tag
]
325 verboseArg
:: [String]
326 verboseArg
= ["--quiet" | verbosity
< Verbosity
.normal
]
331 -> [(SourceRepositoryPackage f
, FilePath)]
332 -> IO [MonitorFilePath
]
333 vcsSyncRepos _v _p _rs
= fail "sync repo not yet supported for bzr"
335 bzrProgram
:: Program
337 (simpleProgram
"bzr")
338 { programFindVersion
= findProgramVersion
"--version" $ \str
->
340 -- "Bazaar (bzr) 2.6.0\n ... lots of extra stuff"
341 (_
: _
: ver
: _
) -> ver
345 -- | VCS driver for Darcs.
346 vcsDarcs
:: VCS Program
349 { vcsRepoType
= KnownRepoType Darcs
350 , vcsProgram
= darcsProgram
358 -> SourceRepositoryPackage f
361 -> [ProgramInvocation
]
362 vcsCloneRepo verbosity prog repo srcuri destdir
=
363 [programInvocation prog cloneArgs
]
365 cloneArgs
:: [String]
366 cloneArgs
= [cloneCmd
, srcuri
, destdir
] ++ tagArgs
++ verboseArg
367 -- At some point the @clone@ command was introduced as an alias for
368 -- @get@, and @clone@ seems to be the recommended one now.
371 | programVersion prog
>= Just
(mkVersion
[2, 8]) =
375 tagArgs
= case srpTag repo
of
377 Just tag
-> ["-t", tag
]
378 verboseArg
:: [String]
379 verboseArg
= ["--quiet" | verbosity
< Verbosity
.normal
]
384 -> [(SourceRepositoryPackage f
, FilePath)]
385 -> IO [MonitorFilePath
]
386 vcsSyncRepos _ _
[] = return []
387 vcsSyncRepos verbosity prog
((primaryRepo
, primaryLocalDir
) : secondaryRepos
) =
389 vcsSyncRepo verbosity prog primaryRepo primaryLocalDir Nothing
390 for_ secondaryRepos
$ \(repo
, localDir
) ->
391 vcsSyncRepo verbosity prog repo localDir
$ Just primaryLocalDir
394 dirs
= primaryLocalDir
: (snd <$> secondaryRepos
)
395 monitors
:: [MonitorFilePath
]
396 monitors
= monitorDirectoryExistence
<$> dirs
398 vcsSyncRepo verbosity prog SourceRepositoryPackage
{..} localDir _peer
=
399 try (lines <$> darcsWithOutput localDir
["log", "--last", "1"]) >>= \case
400 Right
(_
: _
: _
: x
: _
)
401 | Just tag
<- (List
.stripPrefix
"tagged " . List
.dropWhile Char.isSpace) x
402 , Just tag
' <- srpTag
405 Left e |
not (isDoesNotExistError e
) -> throw e
407 removeDirectoryRecursive localDir `
catch` liftA2
unless isDoesNotExistError throw
408 darcs
(takeDirectory localDir
) cloneArgs
410 darcs
:: FilePath -> [String] -> IO ()
411 darcs
= darcs
' runProgramInvocation
413 darcsWithOutput
:: FilePath -> [String] -> IO String
414 darcsWithOutput
= darcs
' getProgramInvocationOutput
416 darcs
' :: (Verbosity
-> ProgramInvocation
-> t
) -> FilePath -> [String] -> t
420 (programInvocation prog args
)
421 { progInvokeCwd
= Just cwd
424 cloneArgs
:: [String]
425 cloneArgs
= ["clone"] ++ tagArgs
++ [srpLocation
, localDir
] ++ verboseArg
427 tagArgs
= case srpTag
of
429 Just tag
-> ["-t" ++ tag
]
430 verboseArg
:: [String]
431 verboseArg
= ["--quiet" | verbosity
< Verbosity
.normal
]
433 darcsProgram
:: Program
435 (simpleProgram
"darcs")
436 { programFindVersion
= findProgramVersion
"--version" $ \str
->
443 -- | VCS driver for Git.
444 vcsGit
:: VCS Program
447 { vcsRepoType
= KnownRepoType Git
448 , vcsProgram
= gitProgram
456 -> SourceRepositoryPackage f
459 -> [ProgramInvocation
]
460 vcsCloneRepo verbosity prog repo srcuri destdir
=
461 [programInvocation prog cloneArgs
]
462 -- And if there's a tag, we have to do that in a second step:
463 ++ [git
(resetArgs tag
) | tag
<- maybeToList (srpTag repo
)]
464 ++ [ git
(["submodule", "sync", "--recursive"] ++ verboseArg
)
465 , git
(["submodule", "update", "--init", "--force", "--recursive"] ++ verboseArg
)
468 git args
= (programInvocation prog args
){progInvokeCwd
= Just destdir
}
470 ["clone", srcuri
, destdir
]
473 branchArgs
= case srpBranch repo
of
474 Just b
-> ["--branch", b
]
476 resetArgs tag
= "reset" : verboseArg
++ ["--hard", tag
, "--"]
477 verboseArg
= ["--quiet" | verbosity
< Verbosity
.normal
]
482 -> [(SourceRepositoryPackage f
, FilePath)]
483 -> IO [MonitorFilePath
]
484 vcsSyncRepos _ _
[] = return []
488 ((primaryRepo
, primaryLocalDir
) : secondaryRepos
) = do
489 vcsSyncRepo verbosity gitProg primaryRepo primaryLocalDir Nothing
491 [ vcsSyncRepo verbosity gitProg repo localDir
(Just primaryLocalDir
)
492 |
(repo
, localDir
) <- secondaryRepos
495 [ monitorDirectoryExistence dir
496 | dir
<- (primaryLocalDir
: map snd secondaryRepos
)
499 vcsSyncRepo verbosity gitProg SourceRepositoryPackage
{..} localDir peer
= do
500 exists
<- doesDirectoryExist localDir
502 then git localDir
["fetch"]
503 else git
(takeDirectory localDir
) cloneArgs
504 -- Before trying to checkout other commits, all submodules must be
505 -- de-initialised and the .git/modules directory must be deleted. This
506 -- is needed because sometimes `git submodule sync` does not actually
507 -- update the submodule source URL. Detailed description here:
508 -- https://git.coop/-/snippets/85
509 git localDir
["submodule", "deinit", "--force", "--all"]
510 let gitModulesDir
= localDir
</> ".git" </> "modules"
511 gitModulesExists
<- doesDirectoryExist gitModulesDir
512 when gitModulesExists
$ removeDirectoryRecursive gitModulesDir
513 git localDir resetArgs
514 git localDir
$ ["submodule", "sync", "--recursive"] ++ verboseArg
515 git localDir
$ ["submodule", "update", "--force", "--init", "--recursive"] ++ verboseArg
516 git localDir
$ ["submodule", "foreach", "--recursive"] ++ verboseArg
++ ["git clean -ffxdq"]
517 git localDir
$ ["clean", "-ffxdq"]
519 git
:: FilePath -> [String] -> IO ()
521 runProgramInvocation verbosity
$
522 (programInvocation gitProg args
)
523 { progInvokeCwd
= Just cwd
527 ["clone", "--no-checkout", loc
, localDir
]
530 Just peerLocalDir
-> ["--reference", peerLocalDir
]
534 resetArgs
= "reset" : verboseArg
++ ["--hard", resetTarget
, "--"]
535 resetTarget
= fromMaybe "HEAD" (srpBranch `mplus` srpTag
)
536 verboseArg
= ["--quiet" | verbosity
< Verbosity
.normal
]
538 gitProgram
:: Program
540 (simpleProgram
"git")
541 { programFindVersion
= findProgramVersion
"--version" $ \str
->
543 -- "git version 2.5.5"
544 (_
: _
: ver
: _
) |
all isTypical ver
-> ver
545 -- or annoyingly "git version 2.17.1.windows.2" yes, really
548 . takeWhile (all isNum
)
554 isNum c
= c
>= '0' && c
<= '9'
555 isTypical c
= isNum c || c
== '.'
556 split cs
= case break (== '.') cs
of
557 (chunk
, []) -> chunk
: []
558 (chunk
, _
: rest
) -> chunk
: split rest
560 -- | VCS driver for Mercurial.
564 { vcsRepoType
= KnownRepoType Mercurial
565 , vcsProgram
= hgProgram
573 -> SourceRepositoryPackage f
576 -> [ProgramInvocation
]
577 vcsCloneRepo verbosity prog repo srcuri destdir
=
578 [programInvocation prog cloneArgs
]
581 ["clone", srcuri
, destdir
]
585 branchArgs
= case srpBranch repo
of
586 Just b
-> ["--branch", b
]
588 tagArgs
= case srpTag repo
of
589 Just t
-> ["--rev", t
]
591 verboseArg
= ["--quiet" | verbosity
< Verbosity
.normal
]
596 -> [(SourceRepositoryPackage f
, FilePath)]
597 -> IO [MonitorFilePath
]
598 vcsSyncRepos _ _
[] = return []
602 ((primaryRepo
, primaryLocalDir
) : secondaryRepos
) = do
603 vcsSyncRepo verbosity hgProg primaryRepo primaryLocalDir
605 [ vcsSyncRepo verbosity hgProg repo localDir
606 |
(repo
, localDir
) <- secondaryRepos
609 [ monitorDirectoryExistence dir
610 | dir
<- (primaryLocalDir
: map snd secondaryRepos
)
612 vcsSyncRepo verbosity hgProg repo localDir
= do
613 exists
<- doesDirectoryExist localDir
615 then hg localDir
["pull"]
616 else hg
(takeDirectory localDir
) cloneArgs
617 hg localDir checkoutArgs
619 hg
:: FilePath -> [String] -> IO ()
621 runProgramInvocation verbosity
$
622 (programInvocation hgProg args
)
623 { progInvokeCwd
= Just cwd
626 ["clone", "--noupdate", (srpLocation repo
), localDir
]
628 verboseArg
= ["--quiet" | verbosity
< Verbosity
.normal
]
630 ["checkout", "--clean"]
632 tagArgs
= case srpTag repo
of
633 Just t
-> ["--rev", t
]
639 { programFindVersion
= findProgramVersion
"--version" $ \str
->
641 -- Mercurial Distributed SCM (version 3.5.2)\n ... long message
642 (_
: _
: _
: _
: ver
: _
) -> takeWhile (\c
-> Char.isDigit c || c
== '.') ver
646 -- | VCS driver for Subversion.
647 vcsSvn
:: VCS Program
650 { vcsRepoType
= KnownRepoType SVN
651 , vcsProgram
= svnProgram
659 -> SourceRepositoryPackage f
662 -> [ProgramInvocation
]
663 vcsCloneRepo verbosity prog _repo srcuri destdir
=
664 [programInvocation prog checkoutArgs
]
666 checkoutArgs
= ["checkout", srcuri
, destdir
] ++ verboseArg
667 verboseArg
= ["--quiet" | verbosity
< Verbosity
.normal
]
668 -- TODO: branch or tag?
673 -> [(SourceRepositoryPackage f
, FilePath)]
674 -> IO [MonitorFilePath
]
675 vcsSyncRepos _v _p _rs
= fail "sync repo not yet supported for svn"
677 svnProgram
:: Program
679 (simpleProgram
"svn")
680 { programFindVersion
= findProgramVersion
"--version" $ \str
->
682 -- svn, version 1.9.4 (r1740329)\n ... long message
683 (_
: _
: ver
: _
) -> ver
687 -- | VCS driver for Pijul.
688 -- Documentation for Pijul can be found at <https://pijul.org/manual/introduction.html>
692 -- As far as I understand pijul, there are branches and "tags" in pijul,
693 -- but there aren't a "commit hash" identifying an arbitrary state.
695 -- One can create `a pijul tag`, which will make a patch hash,
696 -- which depends on everything currently in the repository.
697 -- I guess if you try to apply that patch, you'll be forced to apply
698 -- all the dependencies too. In other words, there are no named tags.
700 -- It's not clear to me whether there is an option to
701 -- "apply this patch *and* all of its dependencies".
702 -- And relatedly, whether how to make sure that there are no other
705 -- With branches it's easier, as you can `pull` and `checkout` them,
706 -- and they seem to be similar enough. Yet, pijul documentations says
708 -- > Note that the purpose of branches in Pijul is quite different from Git,
709 -- since Git's "feature branches" can usually be implemented by just
712 -- I guess it means that indeed instead of creating a branch and making PR
713 -- in "GitHub" workflow, you'd just create a patch and offer it.
714 -- You can do that with `git` too. Push (a branch with) commit to remote
715 -- and ask other to cherry-pick that commit. Yet, in git identity of commit
716 -- changes when it applied to other trees, where patches in pijul have
717 -- will continue to have the same hash.
719 -- Unfortunately pijul doesn't talk about conflict resolution.
720 -- It seems that you get something like:
725 -- Unresolved conflicts:
726 -- (fix conflicts and record the resolution with "pijul record ...")
732 -- >> >>>>>>>>>>>>>>>>>>>>>>>>>>>>>
734 -- ================================
736 -- <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
739 -- And then the `pijul dependencies` would draw you a graph like
742 -- -----> foo on branch B ----->
743 -- resolve conflict Initial patch
744 -- -----> foo on branch A ----->
746 -- Which is seems reasonable.
748 -- So currently, pijul support is very experimental, and most likely
749 -- won't work, even the basics are in place. Tests are also written
750 -- but disabled, as the branching model differs from `git` one,
751 -- for which tests are written.
752 vcsPijul
:: VCS Program
755 { vcsRepoType
= KnownRepoType Pijul
756 , vcsProgram
= pijulProgram
763 -- \^ it seems that pijul does not have verbose flag
765 -> SourceRepositoryPackage f
768 -> [ProgramInvocation
]
769 vcsCloneRepo _verbosity prog repo srcuri destdir
=
770 [programInvocation prog cloneArgs
]
771 -- And if there's a tag, we have to do that in a second step:
772 ++ [ (programInvocation prog
(checkoutArgs tag
))
773 { progInvokeCwd
= Just destdir
775 | tag
<- maybeToList (srpTag repo
)
778 cloneArgs
:: [String]
780 ["clone", srcuri
, destdir
]
782 branchArgs
:: [String]
783 branchArgs
= case srpBranch repo
of
784 Just b
-> ["--from-branch", b
]
786 checkoutArgs tag
= "checkout" : [tag
] -- TODO: this probably doesn't work either
790 -> [(SourceRepositoryPackage f
, FilePath)]
791 -> IO [MonitorFilePath
]
792 vcsSyncRepos _ _
[] = return []
796 ((primaryRepo
, primaryLocalDir
) : secondaryRepos
) = do
797 vcsSyncRepo verbosity pijulProg primaryRepo primaryLocalDir Nothing
799 [ vcsSyncRepo verbosity pijulProg repo localDir
(Just primaryLocalDir
)
800 |
(repo
, localDir
) <- secondaryRepos
803 [ monitorDirectoryExistence dir
804 | dir
<- (primaryLocalDir
: map snd secondaryRepos
)
807 vcsSyncRepo verbosity pijulProg SourceRepositoryPackage
{..} localDir peer
= do
808 exists
<- doesDirectoryExist localDir
810 then pijul localDir
["pull"] -- TODO: this probably doesn't work.
811 else pijul
(takeDirectory localDir
) cloneArgs
812 pijul localDir checkoutArgs
814 pijul
:: FilePath -> [String] -> IO ()
816 runProgramInvocation verbosity
$
817 (programInvocation pijulProg args
)
818 { progInvokeCwd
= Just cwd
821 cloneArgs
:: [String]
823 ["clone", loc
, localDir
]
826 Just peerLocalDir
-> [peerLocalDir
]
829 checkoutArgs
:: [String]
830 checkoutArgs
= "checkout" : ["--force", checkoutTarget
, "--"]
831 checkoutTarget
= fromMaybe "HEAD" (srpBranch `mplus` srpTag
) -- TODO: this is definitely wrong.
833 pijulProgram
:: Program
835 (simpleProgram
"pijul")
836 { programFindVersion
= findProgramVersion
"--version" $ \str
->
839 (_
: ver
: _
) |
all isTypical ver
-> ver
843 isNum c
= c
>= '0' && c
<= '9'
844 isTypical c
= isNum c || c
== '.'