Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F68721895
PhabricatorRepository.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Fri, Jun 28, 16:04
Size
76 KB
Mime Type
text/x-php
Expires
Sun, Jun 30, 16:04 (2 d)
Engine
blob
Format
Raw Data
Handle
18624655
Attached To
rPH Phabricator
PhabricatorRepository.php
View Options
<?php
/**
* @task uri Repository URI Management
* @task autoclose Autoclose
* @task sync Cluster Synchronization
*/
final
class
PhabricatorRepository
extends
PhabricatorRepositoryDAO
implements
PhabricatorApplicationTransactionInterface
,
PhabricatorPolicyInterface
,
PhabricatorFlaggableInterface
,
PhabricatorMarkupInterface
,
PhabricatorDestructibleInterface
,
PhabricatorProjectInterface
,
PhabricatorSpacesInterface
,
PhabricatorConduitResultInterface
{
/**
* Shortest hash we'll recognize in raw "a829f32" form.
*/
const
MINIMUM_UNQUALIFIED_HASH
=
7
;
/**
* Shortest hash we'll recognize in qualified "rXab7ef2f8" form.
*/
const
MINIMUM_QUALIFIED_HASH
=
5
;
const
TABLE_PATH
=
'repository_path'
;
const
TABLE_PATHCHANGE
=
'repository_pathchange'
;
const
TABLE_FILESYSTEM
=
'repository_filesystem'
;
const
TABLE_SUMMARY
=
'repository_summary'
;
const
TABLE_BADCOMMIT
=
'repository_badcommit'
;
const
TABLE_LINTMESSAGE
=
'repository_lintmessage'
;
const
TABLE_PARENTS
=
'repository_parents'
;
const
TABLE_COVERAGE
=
'repository_coverage'
;
const
SERVE_OFF
=
'off'
;
const
SERVE_READONLY
=
'readonly'
;
const
SERVE_READWRITE
=
'readwrite'
;
const
BECAUSE_REPOSITORY_IMPORTING
=
'auto/importing'
;
const
BECAUSE_AUTOCLOSE_DISABLED
=
'auto/disabled'
;
const
BECAUSE_NOT_ON_AUTOCLOSE_BRANCH
=
'auto/nobranch'
;
const
BECAUSE_BRANCH_UNTRACKED
=
'auto/notrack'
;
const
BECAUSE_BRANCH_NOT_AUTOCLOSE
=
'auto/noclose'
;
const
BECAUSE_AUTOCLOSE_FORCED
=
'auto/forced'
;
const
STATUS_ACTIVE
=
'active'
;
const
STATUS_INACTIVE
=
'inactive'
;
protected
$name
;
protected
$callsign
;
protected
$repositorySlug
;
protected
$uuid
;
protected
$viewPolicy
;
protected
$editPolicy
;
protected
$pushPolicy
;
protected
$versionControlSystem
;
protected
$details
=
array
();
protected
$credentialPHID
;
protected
$almanacServicePHID
;
protected
$spacePHID
;
private
$commitCount
=
self
::
ATTACHABLE
;
private
$mostRecentCommit
=
self
::
ATTACHABLE
;
private
$projectPHIDs
=
self
::
ATTACHABLE
;
private
$uris
=
self
::
ATTACHABLE
;
private
$clusterWriteLock
;
private
$clusterWriteVersion
;
public
static
function
initializeNewRepository
(
PhabricatorUser
$actor
)
{
$app
=
id
(
new
PhabricatorApplicationQuery
())
->
setViewer
(
$actor
)
->
withClasses
(
array
(
'PhabricatorDiffusionApplication'
))
->
executeOne
();
$view_policy
=
$app
->
getPolicy
(
DiffusionDefaultViewCapability
::
CAPABILITY
);
$edit_policy
=
$app
->
getPolicy
(
DiffusionDefaultEditCapability
::
CAPABILITY
);
$push_policy
=
$app
->
getPolicy
(
DiffusionDefaultPushCapability
::
CAPABILITY
);
$repository
=
id
(
new
PhabricatorRepository
())
->
setViewPolicy
(
$view_policy
)
->
setEditPolicy
(
$edit_policy
)
->
setPushPolicy
(
$push_policy
)
->
setSpacePHID
(
$actor
->
getDefaultSpacePHID
());
// Put the repository in "Importing" mode until we finish
// parsing it.
$repository
->
setDetail
(
'importing'
,
true
);
return
$repository
;
}
protected
function
getConfiguration
()
{
return
array
(
self
::
CONFIG_AUX_PHID
=>
true
,
self
::
CONFIG_SERIALIZATION
=>
array
(
'details'
=>
self
::
SERIALIZATION_JSON
,
),
self
::
CONFIG_COLUMN_SCHEMA
=>
array
(
'name'
=>
'sort255'
,
'callsign'
=>
'sort32?'
,
'repositorySlug'
=>
'sort64?'
,
'versionControlSystem'
=>
'text32'
,
'uuid'
=>
'text64?'
,
'pushPolicy'
=>
'policy'
,
'credentialPHID'
=>
'phid?'
,
'almanacServicePHID'
=>
'phid?'
,
),
self
::
CONFIG_KEY_SCHEMA
=>
array
(
'callsign'
=>
array
(
'columns'
=>
array
(
'callsign'
),
'unique'
=>
true
,
),
'key_name'
=>
array
(
'columns'
=>
array
(
'name(128)'
),
),
'key_vcs'
=>
array
(
'columns'
=>
array
(
'versionControlSystem'
),
),
'key_slug'
=>
array
(
'columns'
=>
array
(
'repositorySlug'
),
'unique'
=>
true
,
),
),
)
+
parent
::
getConfiguration
();
}
public
function
generatePHID
()
{
return
PhabricatorPHID
::
generateNewPHID
(
PhabricatorRepositoryRepositoryPHIDType
::
TYPECONST
);
}
public
static
function
getStatusMap
()
{
return
array
(
self
::
STATUS_ACTIVE
=>
array
(
'name'
=>
pht
(
'Active'
),
'isTracked'
=>
1
,
),
self
::
STATUS_INACTIVE
=>
array
(
'name'
=>
pht
(
'Inactive'
),
'isTracked'
=>
0
,
),
);
}
public
static
function
getStatusNameMap
()
{
return
ipull
(
self
::
getStatusMap
(),
'name'
);
}
public
function
getStatus
()
{
if
(
$this
->
isTracked
())
{
return
self
::
STATUS_ACTIVE
;
}
else
{
return
self
::
STATUS_INACTIVE
;
}
}
public
function
toDictionary
()
{
return
array
(
'id'
=>
$this
->
getID
(),
'name'
=>
$this
->
getName
(),
'phid'
=>
$this
->
getPHID
(),
'callsign'
=>
$this
->
getCallsign
(),
'monogram'
=>
$this
->
getMonogram
(),
'vcs'
=>
$this
->
getVersionControlSystem
(),
'uri'
=>
PhabricatorEnv
::
getProductionURI
(
$this
->
getURI
()),
'remoteURI'
=>
(
string
)
$this
->
getRemoteURI
(),
'description'
=>
$this
->
getDetail
(
'description'
),
'isActive'
=>
$this
->
isTracked
(),
'isHosted'
=>
$this
->
isHosted
(),
'isImporting'
=>
$this
->
isImporting
(),
'encoding'
=>
$this
->
getDefaultTextEncoding
(),
'staging'
=>
array
(
'supported'
=>
$this
->
supportsStaging
(),
'prefix'
=>
'phabricator'
,
'uri'
=>
$this
->
getStagingURI
(),
),
);
}
public
function
getDefaultTextEncoding
()
{
return
$this
->
getDetail
(
'encoding'
,
'UTF-8'
);
}
public
function
getMonogram
()
{
$callsign
=
$this
->
getCallsign
();
if
(
strlen
(
$callsign
))
{
return
"r{$callsign}"
;
}
$id
=
$this
->
getID
();
return
"R{$id}"
;
}
public
function
getDisplayName
()
{
$slug
=
$this
->
getRepositorySlug
();
if
(
strlen
(
$slug
))
{
return
$slug
;
}
return
$this
->
getMonogram
();
}
public
function
getAllMonograms
()
{
$monograms
=
array
();
$monograms
[]
=
'R'
.
$this
->
getID
();
$callsign
=
$this
->
getCallsign
();
if
(
strlen
(
$callsign
))
{
$monograms
[]
=
'r'
.
$callsign
;
}
return
$monograms
;
}
public
function
getDetail
(
$key
,
$default
=
null
)
{
return
idx
(
$this
->
details
,
$key
,
$default
);
}
public
function
getHumanReadableDetail
(
$key
,
$default
=
null
)
{
$value
=
$this
->
getDetail
(
$key
,
$default
);
switch
(
$key
)
{
case
'branch-filter'
:
case
'close-commits-filter'
:
$value
=
array_keys
(
$value
);
$value
=
implode
(
', '
,
$value
);
break
;
}
return
$value
;
}
public
function
setDetail
(
$key
,
$value
)
{
$this
->
details
[
$key
]
=
$value
;
return
$this
;
}
public
function
attachCommitCount
(
$count
)
{
$this
->
commitCount
=
$count
;
return
$this
;
}
public
function
getCommitCount
()
{
return
$this
->
assertAttached
(
$this
->
commitCount
);
}
public
function
attachMostRecentCommit
(
PhabricatorRepositoryCommit
$commit
=
null
)
{
$this
->
mostRecentCommit
=
$commit
;
return
$this
;
}
public
function
getMostRecentCommit
()
{
return
$this
->
assertAttached
(
$this
->
mostRecentCommit
);
}
public
function
getDiffusionBrowseURIForPath
(
PhabricatorUser
$user
,
$path
,
$line
=
null
,
$branch
=
null
)
{
$drequest
=
DiffusionRequest
::
newFromDictionary
(
array
(
'user'
=>
$user
,
'repository'
=>
$this
,
'path'
=>
$path
,
'branch'
=>
$branch
,
));
return
$drequest
->
generateURI
(
array
(
'action'
=>
'browse'
,
'line'
=>
$line
,
));
}
public
function
getLocalPath
()
{
return
$this
->
getDetail
(
'local-path'
);
}
public
function
getSubversionBaseURI
(
$commit
=
null
)
{
$subpath
=
$this
->
getDetail
(
'svn-subpath'
);
if
(!
strlen
(
$subpath
))
{
$subpath
=
null
;
}
return
$this
->
getSubversionPathURI
(
$subpath
,
$commit
);
}
public
function
getSubversionPathURI
(
$path
=
null
,
$commit
=
null
)
{
$vcs
=
$this
->
getVersionControlSystem
();
if
(
$vcs
!=
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
)
{
throw
new
Exception
(
pht
(
'Not a subversion repository!'
));
}
if
(
$this
->
isHosted
())
{
$uri
=
'file://'
.
$this
->
getLocalPath
();
}
else
{
$uri
=
$this
->
getDetail
(
'remote-uri'
);
}
$uri
=
rtrim
(
$uri
,
'/'
);
if
(
strlen
(
$path
))
{
$path
=
rawurlencode
(
$path
);
$path
=
str_replace
(
'%2F'
,
'/'
,
$path
);
$uri
=
$uri
.
'/'
.
ltrim
(
$path
,
'/'
);
}
if
(
$path
!==
null
||
$commit
!==
null
)
{
$uri
.=
'@'
;
}
if
(
$commit
!==
null
)
{
$uri
.=
$commit
;
}
return
$uri
;
}
public
function
attachProjectPHIDs
(
array
$project_phids
)
{
$this
->
projectPHIDs
=
$project_phids
;
return
$this
;
}
public
function
getProjectPHIDs
()
{
return
$this
->
assertAttached
(
$this
->
projectPHIDs
);
}
/**
* Get the name of the directory this repository should clone or checkout
* into. For example, if the repository name is "Example Repository", a
* reasonable name might be "example-repository". This is used to help users
* get reasonable results when cloning repositories, since they generally do
* not want to clone into directories called "X/" or "Example Repository/".
*
* @return string
*/
public
function
getCloneName
()
{
$name
=
$this
->
getRepositorySlug
();
// Make some reasonable effort to produce reasonable default directory
// names from repository names.
if
(!
strlen
(
$name
))
{
$name
=
$this
->
getName
();
$name
=
phutil_utf8_strtolower
(
$name
);
$name
=
preg_replace
(
'@[/ -:]+@'
,
'-'
,
$name
);
$name
=
trim
(
$name
,
'-'
);
if
(!
strlen
(
$name
))
{
$name
=
$this
->
getCallsign
();
}
}
return
$name
;
}
public
static
function
isValidRepositorySlug
(
$slug
)
{
try
{
self
::
assertValidRepositorySlug
(
$slug
);
return
true
;
}
catch
(
Exception
$ex
)
{
return
false
;
}
}
public
static
function
assertValidRepositorySlug
(
$slug
)
{
if
(!
strlen
(
$slug
))
{
throw
new
Exception
(
pht
(
'The empty string is not a valid repository short name. '
.
'Repository short names must be at least one character long.'
));
}
if
(
strlen
(
$slug
)
>
64
)
{
throw
new
Exception
(
pht
(
'The name "%s" is not a valid repository short name. Repository '
.
'short names must not be longer than 64 characters.'
,
$slug
));
}
if
(
preg_match
(
'/[^a-zA-Z0-9._-]/'
,
$slug
))
{
throw
new
Exception
(
pht
(
'The name "%s" is not a valid repository short name. Repository '
.
'short names may only contain letters, numbers, periods, hyphens '
.
'and underscores.'
,
$slug
));
}
if
(!
preg_match
(
'/^[a-zA-Z0-9]/'
,
$slug
))
{
throw
new
Exception
(
pht
(
'The name "%s" is not a valid repository short name. Repository '
.
'short names must begin with a letter or number.'
,
$slug
));
}
if
(!
preg_match
(
'/[a-zA-Z0-9]
\z
/'
,
$slug
))
{
throw
new
Exception
(
pht
(
'The name "%s" is not a valid repository short name. Repository '
.
'short names must end with a letter or number.'
,
$slug
));
}
if
(
preg_match
(
'/__|--|
\\
.
\\
./'
,
$slug
))
{
throw
new
Exception
(
pht
(
'The name "%s" is not a valid repository short name. Repository '
.
'short names must not contain multiple consecutive underscores, '
.
'hyphens, or periods.'
,
$slug
));
}
if
(
preg_match
(
'/^[A-Z]+
\z
/'
,
$slug
))
{
throw
new
Exception
(
pht
(
'The name "%s" is not a valid repository short name. Repository '
.
'short names may not contain only uppercase letters.'
,
$slug
));
}
if
(
preg_match
(
'/^
\d
+
\z
/'
,
$slug
))
{
throw
new
Exception
(
pht
(
'The name "%s" is not a valid repository short name. Repository '
.
'short names may not contain only numbers.'
,
$slug
));
}
}
public
static
function
assertValidCallsign
(
$callsign
)
{
if
(!
strlen
(
$callsign
))
{
throw
new
Exception
(
pht
(
'A repository callsign must be at least one character long.'
));
}
if
(
strlen
(
$callsign
)
>
32
)
{
throw
new
Exception
(
pht
(
'The callsign "%s" is not a valid repository callsign. Callsigns '
.
'must be no more than 32 bytes long.'
,
$callsign
));
}
if
(!
preg_match
(
'/^[A-Z]+
\z
/'
,
$callsign
))
{
throw
new
Exception
(
pht
(
'The callsign "%s" is not a valid repository callsign. Callsigns '
.
'may only contain UPPERCASE letters.'
,
$callsign
));
}
}
/* -( Remote Command Execution )------------------------------------------- */
public
function
execRemoteCommand
(
$pattern
/* , $arg, ... */
)
{
$args
=
func_get_args
();
return
$this
->
newRemoteCommandFuture
(
$args
)->
resolve
();
}
public
function
execxRemoteCommand
(
$pattern
/* , $arg, ... */
)
{
$args
=
func_get_args
();
return
$this
->
newRemoteCommandFuture
(
$args
)->
resolvex
();
}
public
function
getRemoteCommandFuture
(
$pattern
/* , $arg, ... */
)
{
$args
=
func_get_args
();
return
$this
->
newRemoteCommandFuture
(
$args
);
}
public
function
passthruRemoteCommand
(
$pattern
/* , $arg, ... */
)
{
$args
=
func_get_args
();
return
$this
->
newRemoteCommandPassthru
(
$args
)->
execute
();
}
private
function
newRemoteCommandFuture
(
array
$argv
)
{
return
$this
->
newRemoteCommandEngine
(
$argv
)
->
newFuture
();
}
private
function
newRemoteCommandPassthru
(
array
$argv
)
{
return
$this
->
newRemoteCommandEngine
(
$argv
)
->
setPassthru
(
true
)
->
newFuture
();
}
private
function
newRemoteCommandEngine
(
array
$argv
)
{
return
DiffusionCommandEngine
::
newCommandEngine
(
$this
)
->
setArgv
(
$argv
)
->
setCredentialPHID
(
$this
->
getCredentialPHID
())
->
setProtocol
(
$this
->
getRemoteProtocol
());
}
/* -( Local Command Execution )-------------------------------------------- */
public
function
execLocalCommand
(
$pattern
/* , $arg, ... */
)
{
$args
=
func_get_args
();
return
$this
->
newLocalCommandFuture
(
$args
)->
resolve
();
}
public
function
execxLocalCommand
(
$pattern
/* , $arg, ... */
)
{
$args
=
func_get_args
();
return
$this
->
newLocalCommandFuture
(
$args
)->
resolvex
();
}
public
function
getLocalCommandFuture
(
$pattern
/* , $arg, ... */
)
{
$args
=
func_get_args
();
return
$this
->
newLocalCommandFuture
(
$args
);
}
public
function
passthruLocalCommand
(
$pattern
/* , $arg, ... */
)
{
$args
=
func_get_args
();
return
$this
->
newLocalCommandPassthru
(
$args
)->
execute
();
}
private
function
newLocalCommandFuture
(
array
$argv
)
{
$this
->
assertLocalExists
();
$future
=
DiffusionCommandEngine
::
newCommandEngine
(
$this
)
->
setArgv
(
$argv
)
->
newFuture
();
if
(
$this
->
usesLocalWorkingCopy
())
{
$future
->
setCWD
(
$this
->
getLocalPath
());
}
return
$future
;
}
private
function
newLocalCommandPassthru
(
array
$argv
)
{
$this
->
assertLocalExists
();
$future
=
DiffusionCommandEngine
::
newCommandEngine
(
$this
)
->
setArgv
(
$argv
)
->
setPassthru
(
true
)
->
newFuture
();
if
(
$this
->
usesLocalWorkingCopy
())
{
$future
->
setCWD
(
$this
->
getLocalPath
());
}
return
$future
;
}
public
function
getURI
()
{
$callsign
=
$this
->
getCallsign
();
if
(
strlen
(
$callsign
))
{
return
"/diffusion/{$callsign}/"
;
}
$id
=
$this
->
getID
();
return
"/diffusion/{$id}/"
;
}
public
function
getPathURI
(
$path
)
{
return
$this
->
getURI
().
$path
;
}
public
function
getCommitURI
(
$identifier
)
{
$callsign
=
$this
->
getCallsign
();
if
(
strlen
(
$callsign
))
{
return
"/r{$callsign}{$identifier}"
;
}
$id
=
$this
->
getID
();
return
"/R{$id}:{$identifier}"
;
}
public
static
function
parseRepositoryServicePath
(
$request_path
)
{
// NOTE: In Mercurial over SSH, the path will begin without a leading "/",
// so we're matching it optionally.
$patterns
=
array
(
'(^'
.
'(?P<base>/?diffusion/(?P<identifier>[A-Z]+|[0-9]
\d
*))'
.
'(?P<path>(?:/.*)?)'
.
'
\z
)'
,
);
$identifier
=
null
;
foreach
(
$patterns
as
$pattern
)
{
$matches
=
null
;
if
(!
preg_match
(
$pattern
,
$request_path
,
$matches
))
{
continue
;
}
$identifier
=
$matches
[
'identifier'
];
$base
=
$matches
[
'base'
];
$path
=
$matches
[
'path'
];
break
;
}
if
(
$identifier
===
null
)
{
return
null
;
}
return
array
(
'identifier'
=>
$identifier
,
'base'
=>
$base
,
'path'
=>
$path
,
);
}
public
function
getCanonicalPath
(
$request_path
)
{
$standard_pattern
=
'(^'
.
'(?P<prefix>/diffusion/)'
.
'(?P<identifier>[^/]+)'
.
'(?P<suffix>(?:/.*)?)'
.
'
\z
)'
;
$matches
=
null
;
if
(
preg_match
(
$standard_pattern
,
$request_path
,
$matches
))
{
$prefix
=
$matches
[
'prefix'
];
$callsign
=
$this
->
getCallsign
();
if
(
$callsign
)
{
$identifier
=
$callsign
;
}
else
{
$identifier
=
$this
->
getID
();
}
$suffix
=
$matches
[
'suffix'
];
if
(!
strlen
(
$suffix
))
{
$suffix
=
'/'
;
}
return
$prefix
.
$identifier
.
$suffix
;
}
$commit_pattern
=
'(^'
.
'(?P<prefix>/)'
.
'(?P<monogram>'
.
'(?:'
.
'r(?P<repositoryCallsign>[A-Z]+)'
.
'|'
.
'R(?P<repositoryID>[1-9]
\d
*):'
.
')'
.
'(?P<commit>[a-f0-9]+)'
.
')'
.
'
\z
)'
;
$matches
=
null
;
if
(
preg_match
(
$commit_pattern
,
$request_path
,
$matches
))
{
$commit
=
$matches
[
'commit'
];
return
$this
->
getCommitURI
(
$commit
);
}
return
null
;
}
public
function
generateURI
(
array
$params
)
{
$req_branch
=
false
;
$req_commit
=
false
;
$action
=
idx
(
$params
,
'action'
);
switch
(
$action
)
{
case
'history'
:
case
'browse'
:
case
'change'
:
case
'lastmodified'
:
case
'tags'
:
case
'branches'
:
case
'lint'
:
case
'pathtree'
:
case
'refs'
:
break
;
case
'branch'
:
// NOTE: This does not actually require a branch, and won't have one
// in Subversion. Possibly this should be more clear.
break
;
case
'commit'
:
case
'rendering-ref'
:
$req_commit
=
true
;
break
;
default
:
throw
new
Exception
(
pht
(
'Action "%s" is not a valid repository URI action.'
,
$action
));
}
$path
=
idx
(
$params
,
'path'
);
$branch
=
idx
(
$params
,
'branch'
);
$commit
=
idx
(
$params
,
'commit'
);
$line
=
idx
(
$params
,
'line'
);
if
(
$req_commit
&&
!
strlen
(
$commit
))
{
throw
new
Exception
(
pht
(
'Diffusion URI action "%s" requires commit!'
,
$action
));
}
if
(
$req_branch
&&
!
strlen
(
$branch
))
{
throw
new
Exception
(
pht
(
'Diffusion URI action "%s" requires branch!'
,
$action
));
}
if
(
$action
===
'commit'
)
{
return
$this
->
getCommitURI
(
$commit
);
}
$identifier
=
$this
->
getID
();
$callsign
=
$this
->
getCallsign
();
if
(
$callsign
!==
null
)
{
$identifier
=
$callsign
;
}
if
(
strlen
(
$identifier
))
{
$identifier
=
phutil_escape_uri_path_component
(
$identifier
);
}
if
(
strlen
(
$path
))
{
$path
=
ltrim
(
$path
,
'/'
);
$path
=
str_replace
(
array
(
';'
,
'$'
),
array
(
';;'
,
'$$'
),
$path
);
$path
=
phutil_escape_uri
(
$path
);
}
if
(
strlen
(
$branch
))
{
$branch
=
phutil_escape_uri_path_component
(
$branch
);
$path
=
"{$branch}/{$path}"
;
}
if
(
strlen
(
$commit
))
{
$commit
=
str_replace
(
'$'
,
'$$'
,
$commit
);
$commit
=
';'
.
phutil_escape_uri
(
$commit
);
}
if
(
strlen
(
$line
))
{
$line
=
'$'
.
phutil_escape_uri
(
$line
);
}
switch
(
$action
)
{
case
'change'
:
case
'history'
:
case
'browse'
:
case
'lastmodified'
:
case
'tags'
:
case
'branches'
:
case
'lint'
:
case
'pathtree'
:
case
'refs'
:
$uri
=
"/diffusion/{$identifier}/{$action}/{$path}{$commit}{$line}"
;
break
;
case
'branch'
:
if
(
strlen
(
$path
))
{
$uri
=
"/diffusion/{$identifier}/repository/{$path}"
;
}
else
{
$uri
=
"/diffusion/{$identifier}/"
;
}
break
;
case
'external'
:
$commit
=
ltrim
(
$commit
,
';'
);
$uri
=
"/diffusion/external/{$commit}/"
;
break
;
case
'rendering-ref'
:
// This isn't a real URI per se, it's passed as a query parameter to
// the ajax changeset stuff but then we parse it back out as though
// it came from a URI.
$uri
=
rawurldecode
(
"{$path}{$commit}"
);
break
;
}
if
(
$action
==
'rendering-ref'
)
{
return
$uri
;
}
$uri
=
new
PhutilURI
(
$uri
);
if
(
isset
(
$params
[
'lint'
]))
{
$params
[
'params'
]
=
idx
(
$params
,
'params'
,
array
())
+
array
(
'lint'
=>
$params
[
'lint'
],
);
}
if
(
idx
(
$params
,
'params'
))
{
$uri
->
setQueryParams
(
$params
[
'params'
]);
}
return
$uri
;
}
public
function
updateURIIndex
()
{
$uris
=
array
(
(
string
)
$this
->
getCloneURIObject
(),
);
foreach
(
$uris
as
$key
=>
$uri
)
{
$uris
[
$key
]
=
$this
->
getNormalizedURI
(
$uri
)
->
getNormalizedPath
();
}
PhabricatorRepositoryURIIndex
::
updateRepositoryURIs
(
$this
->
getPHID
(),
$uris
);
return
$this
;
}
private
function
getNormalizedURI
(
$uri
)
{
switch
(
$this
->
getVersionControlSystem
())
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
:
return
new
PhabricatorRepositoryURINormalizer
(
PhabricatorRepositoryURINormalizer
::
TYPE_GIT
,
$uri
);
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
:
return
new
PhabricatorRepositoryURINormalizer
(
PhabricatorRepositoryURINormalizer
::
TYPE_SVN
,
$uri
);
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
:
return
new
PhabricatorRepositoryURINormalizer
(
PhabricatorRepositoryURINormalizer
::
TYPE_MERCURIAL
,
$uri
);
default
:
throw
new
Exception
(
pht
(
'Unrecognized version control system.'
));
}
}
public
function
isTracked
()
{
$status
=
$this
->
getDetail
(
'tracking-enabled'
);
$map
=
self
::
getStatusMap
();
$spec
=
idx
(
$map
,
$status
);
if
(!
$spec
)
{
if
(
$status
)
{
$status
=
self
::
STATUS_ACTIVE
;
}
else
{
$status
=
self
::
STATUS_INACTIVE
;
}
$spec
=
idx
(
$map
,
$status
);
}
return
(
bool
)
idx
(
$spec
,
'isTracked'
,
false
);
}
public
function
getDefaultBranch
()
{
$default
=
$this
->
getDetail
(
'default-branch'
);
if
(
strlen
(
$default
))
{
return
$default
;
}
$default_branches
=
array
(
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
=>
'master'
,
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
=>
'default'
,
);
return
idx
(
$default_branches
,
$this
->
getVersionControlSystem
());
}
public
function
getDefaultArcanistBranch
()
{
return
coalesce
(
$this
->
getDefaultBranch
(),
'svn'
);
}
private
function
isBranchInFilter
(
$branch
,
$filter_key
)
{
$vcs
=
$this
->
getVersionControlSystem
();
$is_git
=
(
$vcs
==
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
);
$use_filter
=
(
$is_git
);
if
(!
$use_filter
)
{
// If this VCS doesn't use filters, pass everything through.
return
true
;
}
$filter
=
$this
->
getDetail
(
$filter_key
,
array
());
// If there's no filter set, let everything through.
if
(!
$filter
)
{
return
true
;
}
// If this branch isn't literally named `regexp(...)`, and it's in the
// filter list, let it through.
if
(
isset
(
$filter
[
$branch
]))
{
if
(
self
::
extractBranchRegexp
(
$branch
)
===
null
)
{
return
true
;
}
}
// If the branch matches a regexp, let it through.
foreach
(
$filter
as
$pattern
=>
$ignored
)
{
$regexp
=
self
::
extractBranchRegexp
(
$pattern
);
if
(
$regexp
!==
null
)
{
if
(
preg_match
(
$regexp
,
$branch
))
{
return
true
;
}
}
}
// Nothing matched, so filter this branch out.
return
false
;
}
public
static
function
extractBranchRegexp
(
$pattern
)
{
$matches
=
null
;
if
(
preg_match
(
'/^regexp
\\
((.*)
\\
)
\z
/'
,
$pattern
,
$matches
))
{
return
$matches
[
1
];
}
return
null
;
}
public
function
shouldTrackBranch
(
$branch
)
{
return
$this
->
isBranchInFilter
(
$branch
,
'branch-filter'
);
}
public
function
formatCommitName
(
$commit_identifier
,
$local
=
false
)
{
$vcs
=
$this
->
getVersionControlSystem
();
$type_git
=
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
;
$type_hg
=
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
;
$is_git
=
(
$vcs
==
$type_git
);
$is_hg
=
(
$vcs
==
$type_hg
);
if
(
$is_git
||
$is_hg
)
{
$name
=
substr
(
$commit_identifier
,
0
,
12
);
$need_scope
=
false
;
}
else
{
$name
=
$commit_identifier
;
$need_scope
=
true
;
}
if
(!
$local
)
{
$need_scope
=
true
;
}
if
(
$need_scope
)
{
$callsign
=
$this
->
getCallsign
();
if
(
$callsign
)
{
$scope
=
"r{$callsign}"
;
}
else
{
$id
=
$this
->
getID
();
$scope
=
"R{$id}:"
;
}
$name
=
$scope
.
$name
;
}
return
$name
;
}
public
function
isImporting
()
{
return
(
bool
)
$this
->
getDetail
(
'importing'
,
false
);
}
public
function
loadImportProgress
()
{
$progress
=
queryfx_all
(
$this
->
establishConnection
(
'r'
),
'SELECT importStatus, count(*) N FROM %T WHERE repositoryID = %d
GROUP BY importStatus'
,
id
(
new
PhabricatorRepositoryCommit
())->
getTableName
(),
$this
->
getID
());
$done
=
0
;
$total
=
0
;
foreach
(
$progress
as
$row
)
{
$total
+=
$row
[
'N'
]
*
4
;
$status
=
$row
[
'importStatus'
];
if
(
$status
&
PhabricatorRepositoryCommit
::
IMPORTED_MESSAGE
)
{
$done
+=
$row
[
'N'
];
}
if
(
$status
&
PhabricatorRepositoryCommit
::
IMPORTED_CHANGE
)
{
$done
+=
$row
[
'N'
];
}
if
(
$status
&
PhabricatorRepositoryCommit
::
IMPORTED_OWNERS
)
{
$done
+=
$row
[
'N'
];
}
if
(
$status
&
PhabricatorRepositoryCommit
::
IMPORTED_HERALD
)
{
$done
+=
$row
[
'N'
];
}
}
if
(
$total
)
{
$ratio
=
(
$done
/
$total
);
}
else
{
$ratio
=
0
;
}
// Cap this at "99.99%", because it's confusing to users when the actual
// fraction is "99.996%" and it rounds up to "100.00%".
if
(
$ratio
>
0.9999
)
{
$ratio
=
0.9999
;
}
return
$ratio
;
}
/**
* Should this repository publish feed, notifications, audits, and email?
*
* We do not publish information about repositories during initial import,
* or if the repository has been set not to publish.
*/
public
function
shouldPublish
()
{
if
(
$this
->
isImporting
())
{
return
false
;
}
if
(
$this
->
getDetail
(
'herald-disabled'
))
{
return
false
;
}
return
true
;
}
/* -( Autoclose )---------------------------------------------------------- */
/**
* Determine if autoclose is active for a branch.
*
* For more details about why, use @{method:shouldSkipAutocloseBranch}.
*
* @param string Branch name to check.
* @return bool True if autoclose is active for the branch.
* @task autoclose
*/
public
function
shouldAutocloseBranch
(
$branch
)
{
return
(
$this
->
shouldSkipAutocloseBranch
(
$branch
)
===
null
);
}
/**
* Determine if autoclose is active for a commit.
*
* For more details about why, use @{method:shouldSkipAutocloseCommit}.
*
* @param PhabricatorRepositoryCommit Commit to check.
* @return bool True if autoclose is active for the commit.
* @task autoclose
*/
public
function
shouldAutocloseCommit
(
PhabricatorRepositoryCommit
$commit
)
{
return
(
$this
->
shouldSkipAutocloseCommit
(
$commit
)
===
null
);
}
/**
* Determine why autoclose should be skipped for a branch.
*
* This method gives a detailed reason why autoclose will be skipped. To
* perform a simple test, use @{method:shouldAutocloseBranch}.
*
* @param string Branch name to check.
* @return const|null Constant identifying reason to skip this branch, or null
* if autoclose is active.
* @task autoclose
*/
public
function
shouldSkipAutocloseBranch
(
$branch
)
{
$all_reason
=
$this
->
shouldSkipAllAutoclose
();
if
(
$all_reason
)
{
return
$all_reason
;
}
if
(!
$this
->
shouldTrackBranch
(
$branch
))
{
return
self
::
BECAUSE_BRANCH_UNTRACKED
;
}
if
(!
$this
->
isBranchInFilter
(
$branch
,
'close-commits-filter'
))
{
return
self
::
BECAUSE_BRANCH_NOT_AUTOCLOSE
;
}
return
null
;
}
/**
* Determine why autoclose should be skipped for a commit.
*
* This method gives a detailed reason why autoclose will be skipped. To
* perform a simple test, use @{method:shouldAutocloseCommit}.
*
* @param PhabricatorRepositoryCommit Commit to check.
* @return const|null Constant identifying reason to skip this commit, or null
* if autoclose is active.
* @task autoclose
*/
public
function
shouldSkipAutocloseCommit
(
PhabricatorRepositoryCommit
$commit
)
{
$all_reason
=
$this
->
shouldSkipAllAutoclose
();
if
(
$all_reason
)
{
return
$all_reason
;
}
switch
(
$this
->
getVersionControlSystem
())
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
:
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
:
return
null
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
:
break
;
default
:
throw
new
Exception
(
pht
(
'Unrecognized version control system.'
));
}
$closeable_flag
=
PhabricatorRepositoryCommit
::
IMPORTED_CLOSEABLE
;
if
(!
$commit
->
isPartiallyImported
(
$closeable_flag
))
{
return
self
::
BECAUSE_NOT_ON_AUTOCLOSE_BRANCH
;
}
return
null
;
}
/**
* Determine why all autoclose operations should be skipped for this
* repository.
*
* @return const|null Constant identifying reason to skip all autoclose
* operations, or null if autoclose operations are not blocked at the
* repository level.
* @task autoclose
*/
private
function
shouldSkipAllAutoclose
()
{
if
(
$this
->
isImporting
())
{
return
self
::
BECAUSE_REPOSITORY_IMPORTING
;
}
if
(
$this
->
getDetail
(
'disable-autoclose'
,
false
))
{
return
self
::
BECAUSE_AUTOCLOSE_DISABLED
;
}
return
null
;
}
/* -( Repository URI Management )------------------------------------------ */
/**
* Get the remote URI for this repository.
*
* @return string
* @task uri
*/
public
function
getRemoteURI
()
{
return
(
string
)
$this
->
getRemoteURIObject
();
}
/**
* Get the remote URI for this repository, including credentials if they're
* used by this repository.
*
* @return PhutilOpaqueEnvelope URI, possibly including credentials.
* @task uri
*/
public
function
getRemoteURIEnvelope
()
{
$uri
=
$this
->
getRemoteURIObject
();
$remote_protocol
=
$this
->
getRemoteProtocol
();
if
(
$remote_protocol
==
'http'
||
$remote_protocol
==
'https'
)
{
// For SVN, we use `--username` and `--password` flags separately, so
// don't add any credentials here.
if
(!
$this
->
isSVN
())
{
$credential_phid
=
$this
->
getCredentialPHID
();
if
(
$credential_phid
)
{
$key
=
PassphrasePasswordKey
::
loadFromPHID
(
$credential_phid
,
PhabricatorUser
::
getOmnipotentUser
());
$uri
->
setUser
(
$key
->
getUsernameEnvelope
()->
openEnvelope
());
$uri
->
setPass
(
$key
->
getPasswordEnvelope
()->
openEnvelope
());
}
}
}
return
new
PhutilOpaqueEnvelope
((
string
)
$uri
);
}
/**
* Get the clone (or checkout) URI for this repository, without authentication
* information.
*
* @return string Repository URI.
* @task uri
*/
public
function
getPublicCloneURI
()
{
$uri
=
$this
->
getCloneURIObject
();
// Make sure we don't leak anything if this repo is using HTTP Basic Auth
// with the credentials in the URI or something zany like that.
// If repository is not accessed over SSH we remove both username and
// password.
if
(!
$this
->
isHosted
())
{
if
(!
$this
->
shouldUseSSH
())
{
$uri
->
setUser
(
null
);
// This might be a Git URI or a normal URI. If it's Git, there's no
// password support.
if
(
$uri
instanceof
PhutilURI
)
{
$uri
->
setPass
(
null
);
}
}
}
return
(
string
)
$uri
;
}
/**
* Get the protocol for the repository's remote.
*
* @return string Protocol, like "ssh" or "git".
* @task uri
*/
public
function
getRemoteProtocol
()
{
$uri
=
$this
->
getRemoteURIObject
();
if
(
$uri
instanceof
PhutilGitURI
)
{
return
'ssh'
;
}
else
{
return
$uri
->
getProtocol
();
}
}
/**
* Get a parsed object representation of the repository's remote URI. This
* may be a normal URI (returned as a @{class@libphutil:PhutilURI}) or a git
* URI (returned as a @{class@libphutil:PhutilGitURI}).
*
* @return wild A @{class@libphutil:PhutilURI} or
* @{class@libphutil:PhutilGitURI}.
* @task uri
*/
public
function
getRemoteURIObject
()
{
$raw_uri
=
$this
->
getDetail
(
'remote-uri'
);
if
(!
$raw_uri
)
{
return
new
PhutilURI
(
''
);
}
if
(!
strncmp
(
$raw_uri
,
'/'
,
1
))
{
return
new
PhutilURI
(
'file://'
.
$raw_uri
);
}
$uri
=
new
PhutilURI
(
$raw_uri
);
if
(
$uri
->
getProtocol
())
{
return
$uri
;
}
$uri
=
new
PhutilGitURI
(
$raw_uri
);
if
(
$uri
->
getDomain
())
{
return
$uri
;
}
throw
new
Exception
(
pht
(
"Remote URI '%s' could not be parsed!"
,
$raw_uri
));
}
/**
* Get the "best" clone/checkout URI for this repository, on any protocol.
*/
public
function
getCloneURIObject
()
{
if
(!
$this
->
isHosted
())
{
if
(
$this
->
isSVN
())
{
// Make sure we pick up the "Import Only" path for Subversion, so
// the user clones the repository starting at the correct path, not
// from the root.
$base_uri
=
$this
->
getSubversionBaseURI
();
$base_uri
=
new
PhutilURI
(
$base_uri
);
$path
=
$base_uri
->
getPath
();
if
(!
$path
)
{
$path
=
'/'
;
}
// If the trailing "@" is not required to escape the URI, strip it for
// readability.
if
(!
preg_match
(
'/@.*@/'
,
$path
))
{
$path
=
rtrim
(
$path
,
'@'
);
}
$base_uri
->
setPath
(
$path
);
return
$base_uri
;
}
else
{
return
$this
->
getRemoteURIObject
();
}
}
// Choose the best URI: pick a read/write URI over a URI which is not
// read/write, and SSH over HTTP.
$serve_ssh
=
$this
->
getServeOverSSH
();
$serve_http
=
$this
->
getServeOverHTTP
();
if
(
$serve_ssh
===
self
::
SERVE_READWRITE
)
{
return
$this
->
getSSHCloneURIObject
();
}
else
if
(
$serve_http
===
self
::
SERVE_READWRITE
)
{
return
$this
->
getHTTPCloneURIObject
();
}
else
if
(
$serve_ssh
!==
self
::
SERVE_OFF
)
{
return
$this
->
getSSHCloneURIObject
();
}
else
if
(
$serve_http
!==
self
::
SERVE_OFF
)
{
return
$this
->
getHTTPCloneURIObject
();
}
else
{
return
null
;
}
}
/**
* Get the repository's SSH clone/checkout URI, if one exists.
*/
public
function
getSSHCloneURIObject
()
{
if
(!
$this
->
isHosted
())
{
if
(
$this
->
shouldUseSSH
())
{
return
$this
->
getRemoteURIObject
();
}
else
{
return
null
;
}
}
$serve_ssh
=
$this
->
getServeOverSSH
();
if
(
$serve_ssh
===
self
::
SERVE_OFF
)
{
return
null
;
}
$uri
=
new
PhutilURI
(
PhabricatorEnv
::
getProductionURI
(
$this
->
getURI
()));
if
(
$this
->
isSVN
())
{
$uri
->
setProtocol
(
'svn+ssh'
);
}
else
{
$uri
->
setProtocol
(
'ssh'
);
}
if
(
$this
->
isGit
())
{
$uri
->
setPath
(
$uri
->
getPath
().
$this
->
getCloneName
().
'.git'
);
}
else
if
(
$this
->
isHg
())
{
$uri
->
setPath
(
$uri
->
getPath
().
$this
->
getCloneName
().
'/'
);
}
$ssh_user
=
AlmanacKeys
::
getClusterSSHUser
();
if
(
strlen
(
$ssh_user
))
{
$uri
->
setUser
(
$ssh_user
);
}
$ssh_host
=
PhabricatorEnv
::
getEnvConfig
(
'diffusion.ssh-host'
);
if
(
strlen
(
$ssh_host
))
{
$uri
->
setDomain
(
$ssh_host
);
}
$uri
->
setPort
(
PhabricatorEnv
::
getEnvConfig
(
'diffusion.ssh-port'
));
return
$uri
;
}
/**
* Get the repository's HTTP clone/checkout URI, if one exists.
*/
public
function
getHTTPCloneURIObject
()
{
if
(!
$this
->
isHosted
())
{
if
(
$this
->
shouldUseHTTP
())
{
return
$this
->
getRemoteURIObject
();
}
else
{
return
null
;
}
}
$serve_http
=
$this
->
getServeOverHTTP
();
if
(
$serve_http
===
self
::
SERVE_OFF
)
{
return
null
;
}
return
$this
->
getRawHTTPCloneURIObject
();
}
private
function
getRawHTTPCloneURIObject
()
{
$uri
=
PhabricatorEnv
::
getProductionURI
(
$this
->
getURI
());
$uri
=
new
PhutilURI
(
$uri
);
if
(
$this
->
isGit
())
{
$uri
->
setPath
(
$uri
->
getPath
().
$this
->
getCloneName
().
'.git'
);
}
else
if
(
$this
->
isHg
())
{
$uri
->
setPath
(
$uri
->
getPath
().
$this
->
getCloneName
().
'/'
);
}
return
$uri
;
}
/**
* Determine if we should connect to the remote using SSH flags and
* credentials.
*
* @return bool True to use the SSH protocol.
* @task uri
*/
private
function
shouldUseSSH
()
{
if
(
$this
->
isHosted
())
{
return
false
;
}
$protocol
=
$this
->
getRemoteProtocol
();
if
(
$this
->
isSSHProtocol
(
$protocol
))
{
return
true
;
}
return
false
;
}
/**
* Determine if we should connect to the remote using HTTP flags and
* credentials.
*
* @return bool True to use the HTTP protocol.
* @task uri
*/
private
function
shouldUseHTTP
()
{
if
(
$this
->
isHosted
())
{
return
false
;
}
$protocol
=
$this
->
getRemoteProtocol
();
return
(
$protocol
==
'http'
||
$protocol
==
'https'
);
}
/**
* Determine if we should connect to the remote using SVN flags and
* credentials.
*
* @return bool True to use the SVN protocol.
* @task uri
*/
private
function
shouldUseSVNProtocol
()
{
if
(
$this
->
isHosted
())
{
return
false
;
}
$protocol
=
$this
->
getRemoteProtocol
();
return
(
$protocol
==
'svn'
);
}
/**
* Determine if a protocol is SSH or SSH-like.
*
* @param string A protocol string, like "http" or "ssh".
* @return bool True if the protocol is SSH-like.
* @task uri
*/
private
function
isSSHProtocol
(
$protocol
)
{
return
(
$protocol
==
'ssh'
||
$protocol
==
'svn+ssh'
);
}
public
function
delete
()
{
$this
->
openTransaction
();
$paths
=
id
(
new
PhabricatorOwnersPath
())
->
loadAllWhere
(
'repositoryPHID = %s'
,
$this
->
getPHID
());
foreach
(
$paths
as
$path
)
{
$path
->
delete
();
}
queryfx
(
$this
->
establishConnection
(
'w'
),
'DELETE FROM %T WHERE repositoryPHID = %s'
,
id
(
new
PhabricatorRepositorySymbol
())->
getTableName
(),
$this
->
getPHID
());
$commits
=
id
(
new
PhabricatorRepositoryCommit
())
->
loadAllWhere
(
'repositoryID = %d'
,
$this
->
getID
());
foreach
(
$commits
as
$commit
)
{
// note PhabricatorRepositoryAuditRequests and
// PhabricatorRepositoryCommitData are deleted here too.
$commit
->
delete
();
}
$mirrors
=
id
(
new
PhabricatorRepositoryMirror
())
->
loadAllWhere
(
'repositoryPHID = %s'
,
$this
->
getPHID
());
foreach
(
$mirrors
as
$mirror
)
{
$mirror
->
delete
();
}
$ref_cursors
=
id
(
new
PhabricatorRepositoryRefCursor
())
->
loadAllWhere
(
'repositoryPHID = %s'
,
$this
->
getPHID
());
foreach
(
$ref_cursors
as
$cursor
)
{
$cursor
->
delete
();
}
$conn_w
=
$this
->
establishConnection
(
'w'
);
queryfx
(
$conn_w
,
'DELETE FROM %T WHERE repositoryID = %d'
,
self
::
TABLE_FILESYSTEM
,
$this
->
getID
());
queryfx
(
$conn_w
,
'DELETE FROM %T WHERE repositoryID = %d'
,
self
::
TABLE_PATHCHANGE
,
$this
->
getID
());
queryfx
(
$conn_w
,
'DELETE FROM %T WHERE repositoryID = %d'
,
self
::
TABLE_SUMMARY
,
$this
->
getID
());
$result
=
parent
::
delete
();
$this
->
saveTransaction
();
return
$result
;
}
public
function
isGit
()
{
$vcs
=
$this
->
getVersionControlSystem
();
return
(
$vcs
==
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
);
}
public
function
isSVN
()
{
$vcs
=
$this
->
getVersionControlSystem
();
return
(
$vcs
==
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
);
}
public
function
isHg
()
{
$vcs
=
$this
->
getVersionControlSystem
();
return
(
$vcs
==
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
);
}
public
function
isHosted
()
{
return
(
bool
)
$this
->
getDetail
(
'hosting-enabled'
,
false
);
}
public
function
setHosted
(
$enabled
)
{
return
$this
->
setDetail
(
'hosting-enabled'
,
$enabled
);
}
public
function
getServeOverHTTP
()
{
if
(
$this
->
isSVN
())
{
return
self
::
SERVE_OFF
;
}
$serve
=
$this
->
getDetail
(
'serve-over-http'
,
self
::
SERVE_OFF
);
return
$this
->
normalizeServeConfigSetting
(
$serve
);
}
public
function
setServeOverHTTP
(
$mode
)
{
return
$this
->
setDetail
(
'serve-over-http'
,
$mode
);
}
public
function
getServeOverSSH
()
{
$serve
=
$this
->
getDetail
(
'serve-over-ssh'
,
self
::
SERVE_OFF
);
return
$this
->
normalizeServeConfigSetting
(
$serve
);
}
public
function
setServeOverSSH
(
$mode
)
{
return
$this
->
setDetail
(
'serve-over-ssh'
,
$mode
);
}
public
static
function
getProtocolAvailabilityName
(
$constant
)
{
switch
(
$constant
)
{
case
self
::
SERVE_OFF
:
return
pht
(
'Off'
);
case
self
::
SERVE_READONLY
:
return
pht
(
'Read Only'
);
case
self
::
SERVE_READWRITE
:
return
pht
(
'Read/Write'
);
default
:
return
pht
(
'Unknown'
);
}
}
private
function
normalizeServeConfigSetting
(
$value
)
{
switch
(
$value
)
{
case
self
::
SERVE_OFF
:
case
self
::
SERVE_READONLY
:
return
$value
;
case
self
::
SERVE_READWRITE
:
if
(
$this
->
isHosted
())
{
return
self
::
SERVE_READWRITE
;
}
else
{
return
self
::
SERVE_READONLY
;
}
default
:
return
self
::
SERVE_OFF
;
}
}
/**
* Raise more useful errors when there are basic filesystem problems.
*/
private
function
assertLocalExists
()
{
if
(!
$this
->
usesLocalWorkingCopy
())
{
return
;
}
$local
=
$this
->
getLocalPath
();
Filesystem
::
assertExists
(
$local
);
Filesystem
::
assertIsDirectory
(
$local
);
Filesystem
::
assertReadable
(
$local
);
}
/**
* Determine if the working copy is bare or not. In Git, this corresponds
* to `--bare`. In Mercurial, `--noupdate`.
*/
public
function
isWorkingCopyBare
()
{
switch
(
$this
->
getVersionControlSystem
())
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
:
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
:
return
false
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
:
$local
=
$this
->
getLocalPath
();
if
(
Filesystem
::
pathExists
(
$local
.
'/.git'
))
{
return
false
;
}
else
{
return
true
;
}
}
}
public
function
usesLocalWorkingCopy
()
{
switch
(
$this
->
getVersionControlSystem
())
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
:
return
$this
->
isHosted
();
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
:
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
:
return
true
;
}
}
public
function
getHookDirectories
()
{
$directories
=
array
();
if
(!
$this
->
isHosted
())
{
return
$directories
;
}
$root
=
$this
->
getLocalPath
();
switch
(
$this
->
getVersionControlSystem
())
{
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_GIT
:
if
(
$this
->
isWorkingCopyBare
())
{
$directories
[]
=
$root
.
'/hooks/pre-receive-phabricator.d/'
;
}
else
{
$directories
[]
=
$root
.
'/.git/hooks/pre-receive-phabricator.d/'
;
}
break
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_SVN
:
$directories
[]
=
$root
.
'/hooks/pre-commit-phabricator.d/'
;
break
;
case
PhabricatorRepositoryType
::
REPOSITORY_TYPE_MERCURIAL
:
// NOTE: We don't support custom Mercurial hooks for now because they're
// messy and we can't easily just drop a `hooks.d/` directory next to
// the hooks.
break
;
}
return
$directories
;
}
public
function
canDestroyWorkingCopy
()
{
if
(
$this
->
isHosted
())
{
// Never destroy hosted working copies.
return
false
;
}
$default_path
=
PhabricatorEnv
::
getEnvConfig
(
'repository.default-local-path'
);
return
Filesystem
::
isDescendant
(
$this
->
getLocalPath
(),
$default_path
);
}
public
function
canUsePathTree
()
{
return
!
$this
->
isSVN
();
}
public
function
canUseGitLFS
()
{
if
(!
$this
->
isGit
())
{
return
false
;
}
if
(!
$this
->
isHosted
())
{
return
false
;
}
// TODO: Unprototype this feature.
if
(!
PhabricatorEnv
::
getEnvConfig
(
'phabricator.show-prototypes'
))
{
return
false
;
}
return
true
;
}
public
function
getGitLFSURI
(
$path
=
null
)
{
if
(!
$this
->
canUseGitLFS
())
{
throw
new
Exception
(
pht
(
'This repository does not support Git LFS, so Git LFS URIs can '
.
'not be generated for it.'
));
}
$uri
=
$this
->
getRawHTTPCloneURIObject
();
$uri
=
(
string
)
$uri
;
$uri
=
$uri
.
'/'
.
$path
;
return
$uri
;
}
public
function
canMirror
()
{
if
(
$this
->
isGit
()
||
$this
->
isHg
())
{
return
true
;
}
return
false
;
}
public
function
canAllowDangerousChanges
()
{
if
(!
$this
->
isHosted
())
{
return
false
;
}
if
(
$this
->
isGit
()
||
$this
->
isHg
())
{
return
true
;
}
return
false
;
}
public
function
shouldAllowDangerousChanges
()
{
return
(
bool
)
$this
->
getDetail
(
'allow-dangerous-changes'
);
}
public
function
writeStatusMessage
(
$status_type
,
$status_code
,
array
$parameters
=
array
())
{
$table
=
new
PhabricatorRepositoryStatusMessage
();
$conn_w
=
$table
->
establishConnection
(
'w'
);
$table_name
=
$table
->
getTableName
();
if
(
$status_code
===
null
)
{
queryfx
(
$conn_w
,
'DELETE FROM %T WHERE repositoryID = %d AND statusType = %s'
,
$table_name
,
$this
->
getID
(),
$status_type
);
}
else
{
queryfx
(
$conn_w
,
'INSERT INTO %T
(repositoryID, statusType, statusCode, parameters, epoch)
VALUES (%d, %s, %s, %s, %d)
ON DUPLICATE KEY UPDATE
statusCode = VALUES(statusCode),
parameters = VALUES(parameters),
epoch = VALUES(epoch)'
,
$table_name
,
$this
->
getID
(),
$status_type
,
$status_code
,
json_encode
(
$parameters
),
time
());
}
return
$this
;
}
public
static
function
getRemoteURIProtocol
(
$raw_uri
)
{
$uri
=
new
PhutilURI
(
$raw_uri
);
if
(
$uri
->
getProtocol
())
{
return
strtolower
(
$uri
->
getProtocol
());
}
$git_uri
=
new
PhutilGitURI
(
$raw_uri
);
if
(
strlen
(
$git_uri
->
getDomain
())
&&
strlen
(
$git_uri
->
getPath
()))
{
return
'ssh'
;
}
return
null
;
}
public
static
function
assertValidRemoteURI
(
$uri
)
{
if
(
trim
(
$uri
)
!=
$uri
)
{
throw
new
Exception
(
pht
(
'The remote URI has leading or trailing whitespace.'
));
}
$protocol
=
self
::
getRemoteURIProtocol
(
$uri
);
// Catch confusion between Git/SCP-style URIs and normal URIs. See T3619
// for discussion. This is usually a user adding "ssh://" to an implicit
// SSH Git URI.
if
(
$protocol
==
'ssh'
)
{
if
(
preg_match
(
'(^[^:@]+://[^/:]+:[^
\d
])'
,
$uri
))
{
throw
new
Exception
(
pht
(
"The remote URI is not formatted correctly. Remote URIs "
.
"with an explicit protocol should be in the form "
.
"'%s', not '%s'. The '%s' syntax is only valid in SCP-style URIs."
,
'proto://domain/path'
,
'proto://domain:/path'
,
':/path'
));
}
}
switch
(
$protocol
)
{
case
'ssh'
:
case
'http'
:
case
'https'
:
case
'git'
:
case
'svn'
:
case
'svn+ssh'
:
break
;
default
:
// NOTE: We're explicitly rejecting 'file://' because it can be
// used to clone from the working copy of another repository on disk
// that you don't normally have permission to access.
throw
new
Exception
(
pht
(
"The URI protocol is unrecognized. It should begin "
.
"'%s', '%s', '%s', '%s', '%s', '%s', or be in the form '%s'."
,
'ssh://'
,
'http://'
,
'https://'
,
'git://'
,
'svn://'
,
'svn+ssh://'
,
'git@domain.com:path'
));
}
return
true
;
}
/**
* Load the pull frequency for this repository, based on the time since the
* last activity.
*
* We pull rarely used repositories less frequently. This finds the most
* recent commit which is older than the current time (which prevents us from
* spinning on repositories with a silly commit post-dated to some time in
* 2037). We adjust the pull frequency based on when the most recent commit
* occurred.
*
* @param int The minimum update interval to use, in seconds.
* @return int Repository update interval, in seconds.
*/
public
function
loadUpdateInterval
(
$minimum
=
15
)
{
// If a repository is still importing, always pull it as frequently as
// possible. This prevents us from hanging for a long time at 99.9% when
// importing an inactive repository.
if
(
$this
->
isImporting
())
{
return
$minimum
;
}
$window_start
=
(
PhabricatorTime
::
getNow
()
+
$minimum
);
$table
=
id
(
new
PhabricatorRepositoryCommit
());
$last_commit
=
queryfx_one
(
$table
->
establishConnection
(
'r'
),
'SELECT epoch FROM %T
WHERE repositoryID = %d AND epoch <= %d
ORDER BY epoch DESC LIMIT 1'
,
$table
->
getTableName
(),
$this
->
getID
(),
$window_start
);
if
(
$last_commit
)
{
$time_since_commit
=
(
$window_start
-
$last_commit
[
'epoch'
]);
$last_few_days
=
phutil_units
(
'3 days in seconds'
);
if
(
$time_since_commit
<=
$last_few_days
)
{
// For repositories with activity in the recent past, we wait one
// extra second for every 10 minutes since the last commit. This
// shorter backoff is intended to handle weekends and other short
// breaks from development.
$smart_wait
=
(
$time_since_commit
/
600
);
}
else
{
// For repositories without recent activity, we wait one extra second
// for every 4 minutes since the last commit. This longer backoff
// handles rarely used repositories, up to the maximum.
$smart_wait
=
(
$time_since_commit
/
240
);
}
// We'll never wait more than 6 hours to pull a repository.
$longest_wait
=
phutil_units
(
'6 hours in seconds'
);
$smart_wait
=
min
(
$smart_wait
,
$longest_wait
);
$smart_wait
=
max
(
$minimum
,
$smart_wait
);
}
else
{
$smart_wait
=
$minimum
;
}
return
$smart_wait
;
}
/**
* Retrieve the sevice URI for the device hosting this repository.
*
* See @{method:newConduitClient} for a general discussion of interacting
* with repository services. This method provides lower-level resolution of
* services, returning raw URIs.
*
* @param PhabricatorUser Viewing user.
* @param bool `true` to throw if a remote URI would be returned.
* @param list<string> List of allowable protocols.
* @return string|null URI, or `null` for local repositories.
*/
public
function
getAlmanacServiceURI
(
PhabricatorUser
$viewer
,
$never_proxy
,
array
$protocols
)
{
$service
=
$this
->
loadAlmanacService
();
if
(!
$service
)
{
return
null
;
}
$bindings
=
$service
->
getActiveBindings
();
if
(!
$bindings
)
{
throw
new
Exception
(
pht
(
'The Almanac service for this repository is not bound to any '
.
'interfaces.'
));
}
$local_device
=
AlmanacKeys
::
getDeviceID
();
if
(
$never_proxy
&&
!
$local_device
)
{
throw
new
Exception
(
pht
(
'Unable to handle proxied service request. This device is not '
.
'registered, so it can not identify local services. Register '
.
'this device before sending requests here.'
));
}
$protocol_map
=
array_fuse
(
$protocols
);
$uris
=
array
();
foreach
(
$bindings
as
$binding
)
{
$iface
=
$binding
->
getInterface
();
// If we're never proxying this and it's locally satisfiable, return
// `null` to tell the caller to handle it locally. If we're allowed to
// proxy, we skip this check and may proxy the request to ourselves.
// (That proxied request will end up here with proxying forbidden,
// return `null`, and then the request will actually run.)
if
(
$local_device
&&
$never_proxy
)
{
if
(
$iface
->
getDevice
()->
getName
()
==
$local_device
)
{
return
null
;
}
}
$uri
=
$this
->
getClusterRepositoryURIFromBinding
(
$binding
);
$protocol
=
$uri
->
getProtocol
();
if
(
empty
(
$protocol_map
[
$protocol
]))
{
continue
;
}
$uris
[]
=
$uri
;
}
if
(!
$uris
)
{
throw
new
Exception
(
pht
(
'The Almanac service for this repository is not bound to any '
.
'interfaces which support the required protocols (%s).'
,
implode
(
', '
,
$protocols
)));
}
if
(
$never_proxy
)
{
throw
new
Exception
(
pht
(
'Refusing to proxy a repository request from a cluster host. '
.
'Cluster hosts must correctly route their intracluster requests.'
));
}
shuffle
(
$uris
);
return
head
(
$uris
);
}
/**
* Build a new Conduit client in order to make a service call to this
* repository.
*
* If the repository is hosted locally, this method may return `null`. The
* caller should use `ConduitCall` or other local logic to complete the
* request.
*
* By default, we will return a @{class:ConduitClient} for any repository with
* a service, even if that service is on the current device.
*
* We do this because this configuration does not make very much sense in a
* production context, but is very common in a test/development context
* (where the developer's machine is both the web host and the repository
* service). By proxying in development, we get more consistent behavior
* between development and production, and don't have a major untested
* codepath.
*
* The `$never_proxy` parameter can be used to prevent this local proxying.
* If the flag is passed:
*
* - The method will return `null` (implying a local service call)
* if the repository service is hosted on the current device.
* - The method will throw if it would need to return a client.
*
* This is used to prevent loops in Conduit: the first request will proxy,
* even in development, but the second request will be identified as a
* cluster request and forced not to proxy.
*
* For lower-level service resolution, see @{method:getAlmanacServiceURI}.
*
* @param PhabricatorUser Viewing user.
* @param bool `true` to throw if a client would be returned.
* @return ConduitClient|null Client, or `null` for local repositories.
*/
public
function
newConduitClient
(
PhabricatorUser
$viewer
,
$never_proxy
=
false
)
{
$uri
=
$this
->
getAlmanacServiceURI
(
$viewer
,
$never_proxy
,
array
(
'http'
,
'https'
,
));
if
(
$uri
===
null
)
{
return
null
;
}
$domain
=
id
(
new
PhutilURI
(
PhabricatorEnv
::
getURI
(
'/'
)))->
getDomain
();
$client
=
id
(
new
ConduitClient
(
$uri
))
->
setHost
(
$domain
);
if
(
$viewer
->
isOmnipotent
())
{
// If the caller is the omnipotent user (normally, a daemon), we will
// sign the request with this host's asymmetric keypair.
$public_path
=
AlmanacKeys
::
getKeyPath
(
'device.pub'
);
try
{
$public_key
=
Filesystem
::
readFile
(
$public_path
);
}
catch
(
Exception
$ex
)
{
throw
new
PhutilAggregateException
(
pht
(
'Unable to read device public key while attempting to make '
.
'authenticated method call within the Phabricator cluster. '
.
'Use `%s` to register keys for this device. Exception: %s'
,
'bin/almanac register'
,
$ex
->
getMessage
()),
array
(
$ex
));
}
$private_path
=
AlmanacKeys
::
getKeyPath
(
'device.key'
);
try
{
$private_key
=
Filesystem
::
readFile
(
$private_path
);
$private_key
=
new
PhutilOpaqueEnvelope
(
$private_key
);
}
catch
(
Exception
$ex
)
{
throw
new
PhutilAggregateException
(
pht
(
'Unable to read device private key while attempting to make '
.
'authenticated method call within the Phabricator cluster. '
.
'Use `%s` to register keys for this device. Exception: %s'
,
'bin/almanac register'
,
$ex
->
getMessage
()),
array
(
$ex
));
}
$client
->
setSigningKeys
(
$public_key
,
$private_key
);
}
else
{
// If the caller is a normal user, we generate or retrieve a cluster
// API token.
$token
=
PhabricatorConduitToken
::
loadClusterTokenForUser
(
$viewer
);
if
(
$token
)
{
$client
->
setConduitToken
(
$token
->
getToken
());
}
}
return
$client
;
}
/* -( Repository URIs )---------------------------------------------------- */
public
function
attachURIs
(
array
$uris
)
{
$custom_map
=
array
();
foreach
(
$uris
as
$key
=>
$uri
)
{
$builtin_key
=
$uri
->
getRepositoryURIBuiltinKey
();
if
(
$builtin_key
!==
null
)
{
$custom_map
[
$builtin_key
]
=
$key
;
}
}
$builtin_uris
=
$this
->
newBuiltinURIs
();
$seen_builtins
=
array
();
foreach
(
$builtin_uris
as
$builtin_uri
)
{
$builtin_key
=
$builtin_uri
->
getRepositoryURIBuiltinKey
();
$seen_builtins
[
$builtin_key
]
=
true
;
// If this builtin URI is disabled, don't attach it and remove the
// persisted version if it exists.
if
(
$builtin_uri
->
getIsDisabled
())
{
if
(
isset
(
$custom_map
[
$builtin_key
]))
{
unset
(
$uris
[
$custom_map
[
$builtin_key
]]);
}
continue
;
}
// If we don't have a persisted version of the URI, add the builtin
// version.
if
(
empty
(
$custom_map
[
$builtin_key
]))
{
$uris
[]
=
$builtin_uri
;
}
}
// Remove any builtins which no longer exist.
foreach
(
$custom_map
as
$builtin_key
=>
$key
)
{
if
(
empty
(
$seen_builtins
[
$builtin_key
]))
{
unset
(
$uris
[
$key
]);
}
}
$this
->
uris
=
$uris
;
return
$this
;
}
public
function
getURIs
()
{
return
$this
->
assertAttached
(
$this
->
uris
);
}
protected
function
newBuiltinURIs
()
{
$has_callsign
=
(
$this
->
getCallsign
()
!==
null
);
$has_shortname
=
(
$this
->
getRepositorySlug
()
!==
null
);
$identifier_map
=
array
(
PhabricatorRepositoryURI
::
BUILTIN_IDENTIFIER_CALLSIGN
=>
$has_callsign
,
PhabricatorRepositoryURI
::
BUILTIN_IDENTIFIER_SHORTNAME
=>
$has_shortname
,
PhabricatorRepositoryURI
::
BUILTIN_IDENTIFIER_ID
=>
true
,
);
$allow_http
=
PhabricatorEnv
::
getEnvConfig
(
'diffusion.allow-http-auth'
);
$base_uri
=
PhabricatorEnv
::
getURI
(
'/'
);
$base_uri
=
new
PhutilURI
(
$base_uri
);
$has_https
=
(
$base_uri
->
getProtocol
()
==
'https'
);
$has_https
=
(
$has_https
&&
$allow_http
);
$has_http
=
!
PhabricatorEnv
::
getEnvConfig
(
'security.require-https'
);
$has_http
=
(
$has_http
&&
$allow_http
);
// TODO: Maybe allow users to disable this by default somehow?
$has_ssh
=
true
;
$protocol_map
=
array
(
PhabricatorRepositoryURI
::
BUILTIN_PROTOCOL_SSH
=>
$has_ssh
,
PhabricatorRepositoryURI
::
BUILTIN_PROTOCOL_HTTPS
=>
$has_https
,
PhabricatorRepositoryURI
::
BUILTIN_PROTOCOL_HTTP
=>
$has_http
,
);
$uris
=
array
();
foreach
(
$protocol_map
as
$protocol
=>
$proto_supported
)
{
foreach
(
$identifier_map
as
$identifier
=>
$id_supported
)
{
$uris
[]
=
PhabricatorRepositoryURI
::
initializeNewURI
(
$this
)
->
setBuiltinProtocol
(
$protocol
)
->
setBuiltinIdentifier
(
$identifier
)
->
setIsDisabled
(!
$proto_supported
||
!
$id_supported
);
}
}
return
$uris
;
}
/* -( Cluster Synchronization )-------------------------------------------- */
private
function
shouldEnableSynchronization
()
{
$service_phid
=
$this
->
getAlmanacServicePHID
();
if
(!
$service_phid
)
{
return
false
;
}
// TODO: For now, this is only supported for Git.
if
(!
$this
->
isGit
())
{
return
false
;
}
// TODO: It may eventually make sense to try to version and synchronize
// observed repositories (so that daemons don't do reads against out-of
// date hosts), but don't bother for now.
if
(!
$this
->
isHosted
())
{
return
false
;
}
$device
=
AlmanacKeys
::
getLiveDevice
();
if
(!
$device
)
{
return
false
;
}
return
true
;
}
/**
* Synchronize repository version information after creating a repository.
*
* This initializes working copy versions for all currently bound devices to
* 0, so that we don't get stuck making an ambiguous choice about which
* devices are leaders when we later synchronize before a read.
*
* @task sync
*/
public
function
synchronizeWorkingCopyAfterCreation
()
{
if
(!
$this
->
shouldEnableSynchronization
())
{
return
;
}
$repository_phid
=
$this
->
getPHID
();
$service
=
$this
->
loadAlmanacService
();
if
(!
$service
)
{
throw
new
Exception
(
pht
(
'Failed to load repository cluster service.'
));
}
$bindings
=
$service
->
getActiveBindings
();
foreach
(
$bindings
as
$binding
)
{
PhabricatorRepositoryWorkingCopyVersion
::
updateVersion
(
$repository_phid
,
$binding
->
getDevicePHID
(),
0
);
}
}
/**
* @task sync
*/
public
function
synchronizeWorkingCopyBeforeRead
()
{
if
(!
$this
->
shouldEnableSynchronization
())
{
return
;
}
$repository_phid
=
$this
->
getPHID
();
$device
=
AlmanacKeys
::
getLiveDevice
();
$device_phid
=
$device
->
getPHID
();
$read_lock
=
PhabricatorRepositoryWorkingCopyVersion
::
getReadLock
(
$repository_phid
,
$device_phid
);
// TODO: Raise a more useful exception if we fail to grab this lock.
$read_lock
->
lock
(
phutil_units
(
'2 minutes in seconds'
));
$versions
=
PhabricatorRepositoryWorkingCopyVersion
::
loadVersions
(
$repository_phid
);
$versions
=
mpull
(
$versions
,
null
,
'getDevicePHID'
);
$this_version
=
idx
(
$versions
,
$device_phid
);
if
(
$this_version
)
{
$this_version
=
(
int
)
$this_version
->
getRepositoryVersion
();
}
else
{
$this_version
=
-
1
;
}
if
(
$versions
)
{
// This is the normal case, where we have some version information and
// can identify which nodes are leaders. If the current node is not a
// leader, we want to fetch from a leader and then update our version.
$max_version
=
(
int
)
max
(
mpull
(
$versions
,
'getRepositoryVersion'
));
if
(
$max_version
>
$this_version
)
{
$fetchable
=
array
();
foreach
(
$versions
as
$version
)
{
if
(
$version
->
getRepositoryVersion
()
==
$max_version
)
{
$fetchable
[]
=
$version
->
getDevicePHID
();
}
}
$this
->
synchronizeWorkingCopyFromDevices
(
$fetchable
);
PhabricatorRepositoryWorkingCopyVersion
::
updateVersion
(
$repository_phid
,
$device_phid
,
$max_version
);
}
$result_version
=
$max_version
;
}
else
{
// If no version records exist yet, we need to be careful, because we
// can not tell which nodes are leaders.
// There might be several nodes with arbitrary existing data, and we have
// no way to tell which one has the "right" data. If we pick wrong, we
// might erase some or all of the data in the repository.
// Since this is dangeorus, we refuse to guess unless there is only one
// device. If we're the only device in the group, we obviously must be
// a leader.
$service
=
$this
->
loadAlmanacService
();
if
(!
$service
)
{
throw
new
Exception
(
pht
(
'Failed to load repository cluster service.'
));
}
$bindings
=
$service
->
getActiveBindings
();
$device_map
=
array
();
foreach
(
$bindings
as
$binding
)
{
$device_map
[
$binding
->
getDevicePHID
()]
=
true
;
}
if
(
count
(
$device_map
)
>
1
)
{
throw
new
Exception
(
pht
(
'Repository "%s" exists on more than one device, but no device '
.
'has any repository version information. Phabricator can not '
.
'guess which copy of the existing data is authoritative. Remove '
.
'all but one device from service to mark the remaining device '
.
'as the authority.'
,
$this
->
getDisplayName
()));
}
if
(
empty
(
$device_map
[
$device
->
getPHID
()]))
{
throw
new
Exception
(
pht
(
'Repository "%s" is being synchronized on device "%s", but '
.
'this device is not bound to the corresponding cluster '
.
'service ("%s").'
,
$this
->
getDisplayName
(),
$device
->
getName
(),
$service
->
getName
()));
}
// The current device is the only device in service, so it must be a
// leader. We can safely have any future nodes which come online read
// from it.
PhabricatorRepositoryWorkingCopyVersion
::
updateVersion
(
$repository_phid
,
$device_phid
,
0
);
$result_version
=
0
;
}
$read_lock
->
unlock
();
return
$result_version
;
}
/**
* @task sync
*/
public
function
synchronizeWorkingCopyBeforeWrite
(
PhabricatorUser
$actor
)
{
if
(!
$this
->
shouldEnableSynchronization
())
{
return
;
}
$repository_phid
=
$this
->
getPHID
();
$device
=
AlmanacKeys
::
getLiveDevice
();
$device_phid
=
$device
->
getPHID
();
$write_lock
=
PhabricatorRepositoryWorkingCopyVersion
::
getWriteLock
(
$repository_phid
);
// TODO: Raise a more useful exception if we fail to grab this lock.
$write_lock
->
lock
(
phutil_units
(
'2 minutes in seconds'
));
$versions
=
PhabricatorRepositoryWorkingCopyVersion
::
loadVersions
(
$repository_phid
);
foreach
(
$versions
as
$version
)
{
if
(!
$version
->
getIsWriting
())
{
continue
;
}
throw
new
Exception
(
pht
(
'An previous write to this repository was interrupted; refusing '
.
'new writes. This issue resolves operator intervention to resolve, '
.
'see "Write Interruptions" in the "Cluster: Repositories" in the '
.
'documentation for instructions.'
));
}
try
{
$max_version
=
$this
->
synchronizeWorkingCopyBeforeRead
();
}
catch
(
Exception
$ex
)
{
$write_lock
->
unlock
();
throw
$ex
;
}
PhabricatorRepositoryWorkingCopyVersion
::
willWrite
(
$repository_phid
,
$device_phid
,
array
(
'userPHID'
=>
$actor
->
getPHID
(),
'epoch'
=>
PhabricatorTime
::
getNow
(),
'devicePHID'
=>
$device_phid
,
));
$this
->
clusterWriteVersion
=
$max_version
;
$this
->
clusterWriteLock
=
$write_lock
;
}
/**
* @task sync
*/
public
function
synchronizeWorkingCopyAfterWrite
()
{
if
(!
$this
->
shouldEnableSynchronization
())
{
return
;
}
if
(!
$this
->
clusterWriteLock
)
{
throw
new
Exception
(
pht
(
'Trying to synchronize after write, but not holding a write '
.
'lock!'
));
}
$repository_phid
=
$this
->
getPHID
();
$device
=
AlmanacKeys
::
getLiveDevice
();
$device_phid
=
$device
->
getPHID
();
// NOTE: This means we're still bumping the version when pushes fail. We
// could select only un-rejected events instead to bump a little less
// often.
$new_log
=
id
(
new
PhabricatorRepositoryPushEventQuery
())
->
setViewer
(
PhabricatorUser
::
getOmnipotentUser
())
->
withRepositoryPHIDs
(
array
(
$repository_phid
))
->
setLimit
(
1
)
->
executeOne
();
$old_version
=
$this
->
clusterWriteVersion
;
if
(
$new_log
)
{
$new_version
=
$new_log
->
getID
();
}
else
{
$new_version
=
$old_version
;
}
PhabricatorRepositoryWorkingCopyVersion
::
didWrite
(
$repository_phid
,
$device_phid
,
$this
->
clusterWriteVersion
,
$new_log
->
getID
());
$this
->
clusterWriteLock
->
unlock
();
$this
->
clusterWriteLock
=
null
;
}
/**
* @task sync
*/
private
function
synchronizeWorkingCopyFromDevices
(
array
$device_phids
)
{
$service
=
$this
->
loadAlmanacService
();
if
(!
$service
)
{
throw
new
Exception
(
pht
(
'Failed to load repository cluster service.'
));
}
$device_map
=
array_fuse
(
$device_phids
);
$bindings
=
$service
->
getActiveBindings
();
$fetchable
=
array
();
foreach
(
$bindings
as
$binding
)
{
// We can't fetch from nodes which don't have the newest version.
$device_phid
=
$binding
->
getDevicePHID
();
if
(
empty
(
$device_map
[
$device_phid
]))
{
continue
;
}
// TODO: For now, only fetch over SSH. We could support fetching over
// HTTP eventually.
if
(
$binding
->
getAlmanacPropertyValue
(
'protocol'
)
!=
'ssh'
)
{
continue
;
}
$fetchable
[]
=
$binding
;
}
if
(!
$fetchable
)
{
throw
new
Exception
(
pht
(
'Leader lost: no up-to-date nodes in repository cluster are '
.
'fetchable.'
));
}
$caught
=
null
;
foreach
(
$fetchable
as
$binding
)
{
try
{
$this
->
synchronizeWorkingCopyFromBinding
(
$binding
);
$caught
=
null
;
break
;
}
catch
(
Exception
$ex
)
{
$caught
=
$ex
;
}
}
if
(
$caught
)
{
throw
$caught
;
}
}
private
function
synchronizeWorkingCopyFromBinding
(
$binding
)
{
$fetch_uri
=
$this
->
getClusterRepositoryURIFromBinding
(
$binding
);
if
(
$this
->
isGit
())
{
$argv
=
array
(
'fetch --prune -- %s %s'
,
$fetch_uri
,
'+refs/*:refs/*'
,
);
}
else
{
throw
new
Exception
(
pht
(
'Binding sync only supported for git!'
));
}
$future
=
DiffusionCommandEngine
::
newCommandEngine
(
$this
)
->
setArgv
(
$argv
)
->
setConnectAsDevice
(
true
)
->
setSudoAsDaemon
(
true
)
->
setProtocol
(
$fetch_uri
->
getProtocol
())
->
newFuture
();
$future
->
setCWD
(
$this
->
getLocalPath
());
$future
->
resolvex
();
}
private
function
getClusterRepositoryURIFromBinding
(
AlmanacBinding
$binding
)
{
$protocol
=
$binding
->
getAlmanacPropertyValue
(
'protocol'
);
if
(
$protocol
===
null
)
{
$protocol
=
'https'
;
}
$iface
=
$binding
->
getInterface
();
$address
=
$iface
->
renderDisplayAddress
();
$path
=
$this
->
getURI
();
return
id
(
new
PhutilURI
(
"{$protocol}://{$address}"
))
->
setPath
(
$path
);
}
public
function
loadAlmanacService
()
{
$service_phid
=
$this
->
getAlmanacServicePHID
();
if
(!
$service_phid
)
{
// No service, so this is a local repository.
return
null
;
}
$service
=
id
(
new
AlmanacServiceQuery
())
->
setViewer
(
PhabricatorUser
::
getOmnipotentUser
())
->
withPHIDs
(
array
(
$service_phid
))
->
needBindings
(
true
)
->
needProperties
(
true
)
->
executeOne
();
if
(!
$service
)
{
throw
new
Exception
(
pht
(
'The Almanac service for this repository is invalid or could not '
.
'be loaded.'
));
}
$service_type
=
$service
->
getServiceImplementation
();
if
(!(
$service_type
instanceof
AlmanacClusterRepositoryServiceType
))
{
throw
new
Exception
(
pht
(
'The Almanac service for this repository does not have the correct '
.
'service type.'
));
}
return
$service
;
}
/* -( Symbols )-------------------------------------------------------------*/
public
function
getSymbolSources
()
{
return
$this
->
getDetail
(
'symbol-sources'
,
array
());
}
public
function
getSymbolLanguages
()
{
return
$this
->
getDetail
(
'symbol-languages'
,
array
());
}
/* -( Staging )------------------------------------------------------------ */
public
function
supportsStaging
()
{
return
$this
->
isGit
();
}
public
function
getStagingURI
()
{
if
(!
$this
->
supportsStaging
())
{
return
null
;
}
return
$this
->
getDetail
(
'staging-uri'
,
null
);
}
/* -( Automation )--------------------------------------------------------- */
public
function
supportsAutomation
()
{
return
$this
->
isGit
();
}
public
function
canPerformAutomation
()
{
if
(!
$this
->
supportsAutomation
())
{
return
false
;
}
if
(!
$this
->
getAutomationBlueprintPHIDs
())
{
return
false
;
}
return
true
;
}
public
function
getAutomationBlueprintPHIDs
()
{
if
(!
$this
->
supportsAutomation
())
{
return
array
();
}
return
$this
->
getDetail
(
'automation.blueprintPHIDs'
,
array
());
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public
function
getApplicationTransactionEditor
()
{
return
new
PhabricatorRepositoryEditor
();
}
public
function
getApplicationTransactionObject
()
{
return
$this
;
}
public
function
getApplicationTransactionTemplate
()
{
return
new
PhabricatorRepositoryTransaction
();
}
public
function
willRenderTimeline
(
PhabricatorApplicationTransactionView
$timeline
,
AphrontRequest
$request
)
{
return
$timeline
;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public
function
getCapabilities
()
{
return
array
(
PhabricatorPolicyCapability
::
CAN_VIEW
,
PhabricatorPolicyCapability
::
CAN_EDIT
,
DiffusionPushCapability
::
CAPABILITY
,
);
}
public
function
getPolicy
(
$capability
)
{
switch
(
$capability
)
{
case
PhabricatorPolicyCapability
::
CAN_VIEW
:
return
$this
->
getViewPolicy
();
case
PhabricatorPolicyCapability
::
CAN_EDIT
:
return
$this
->
getEditPolicy
();
case
DiffusionPushCapability
::
CAPABILITY
:
return
$this
->
getPushPolicy
();
}
}
public
function
hasAutomaticCapability
(
$capability
,
PhabricatorUser
$user
)
{
return
false
;
}
public
function
describeAutomaticCapability
(
$capability
)
{
return
null
;
}
/* -( PhabricatorMarkupInterface )----------------------------------------- */
public
function
getMarkupFieldKey
(
$field
)
{
$hash
=
PhabricatorHash
::
digestForIndex
(
$this
->
getMarkupText
(
$field
));
return
"repo:{$hash}"
;
}
public
function
newMarkupEngine
(
$field
)
{
return
PhabricatorMarkupEngine
::
newMarkupEngine
(
array
());
}
public
function
getMarkupText
(
$field
)
{
return
$this
->
getDetail
(
'description'
);
}
public
function
didMarkupText
(
$field
,
$output
,
PhutilMarkupEngine
$engine
)
{
require_celerity_resource
(
'phabricator-remarkup-css'
);
return
phutil_tag
(
'div'
,
array
(
'class'
=>
'phabricator-remarkup'
,
),
$output
);
}
public
function
shouldUseMarkupCache
(
$field
)
{
return
true
;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public
function
destroyObjectPermanently
(
PhabricatorDestructionEngine
$engine
)
{
$phid
=
$this
->
getPHID
();
$this
->
openTransaction
();
$this
->
delete
();
PhabricatorRepositoryURIIndex
::
updateRepositoryURIs
(
$phid
,
array
());
$books
=
id
(
new
DivinerBookQuery
())
->
setViewer
(
$engine
->
getViewer
())
->
withRepositoryPHIDs
(
array
(
$phid
))
->
execute
();
foreach
(
$books
as
$book
)
{
$engine
->
destroyObject
(
$book
);
}
$atoms
=
id
(
new
DivinerAtomQuery
())
->
setViewer
(
$engine
->
getViewer
())
->
withRepositoryPHIDs
(
array
(
$phid
))
->
execute
();
foreach
(
$atoms
as
$atom
)
{
$engine
->
destroyObject
(
$atom
);
}
$lfs_refs
=
id
(
new
PhabricatorRepositoryGitLFSRefQuery
())
->
setViewer
(
$engine
->
getViewer
())
->
withRepositoryPHIDs
(
array
(
$phid
))
->
execute
();
foreach
(
$lfs_refs
as
$ref
)
{
$engine
->
destroyObject
(
$ref
);
}
$this
->
saveTransaction
();
}
/* -( PhabricatorSpacesInterface )----------------------------------------- */
public
function
getSpacePHID
()
{
return
$this
->
spacePHID
;
}
/* -( PhabricatorConduitResultInterface )---------------------------------- */
public
function
getFieldSpecificationsForConduit
()
{
return
array
(
id
(
new
PhabricatorConduitSearchFieldSpecification
())
->
setKey
(
'name'
)
->
setType
(
'string'
)
->
setDescription
(
pht
(
'The repository name.'
)),
id
(
new
PhabricatorConduitSearchFieldSpecification
())
->
setKey
(
'vcs'
)
->
setType
(
'string'
)
->
setDescription
(
pht
(
'The VCS this repository uses ("git", "hg" or "svn").'
)),
id
(
new
PhabricatorConduitSearchFieldSpecification
())
->
setKey
(
'callsign'
)
->
setType
(
'string'
)
->
setDescription
(
pht
(
'The repository callsign, if it has one.'
)),
id
(
new
PhabricatorConduitSearchFieldSpecification
())
->
setKey
(
'shortName'
)
->
setType
(
'string'
)
->
setDescription
(
pht
(
'Unique short name, if the repository has one.'
)),
);
}
public
function
getFieldValuesForConduit
()
{
return
array
(
'name'
=>
$this
->
getName
(),
'vcs'
=>
$this
->
getVersionControlSystem
(),
'callsign'
=>
$this
->
getCallsign
(),
'shortName'
=>
$this
->
getRepositorySlug
(),
);
}
public
function
getConduitSearchAttachments
()
{
return
array
();
}
}
Event Timeline
Log In to Comment